@getdom/studio 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE.md +23 -9
- package/package.json +1 -1
- package/src/pages/_layout/componentCatalog.generated.js +1 -1
- package/src/pages/components/command-palette/DomCommandPalette.vue +6 -1
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +52 -53
- package/src/pages/data/agent-chat/DomAgentConversation.vue +163 -154
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +53 -57
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +8 -9
- package/src/pages/lib/vue/index.d.ts +3 -3
- package/src/tailwind.css +5 -1
- package/src/theme.css +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
More flexible calendar highlights, refined agent chat, and updated website
|
|
6
|
+
licensing flows.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `DomMonthCalendar` now accepts `todayIndicator="both"` to show the cell outline
|
|
11
|
+
and date circle together. `todayDateClass` customises the circle in either
|
|
12
|
+
`circle` or `both` mode, with matching TypeScript declarations and an interactive
|
|
13
|
+
example.
|
|
14
|
+
- Website support for account-optional licence checkout, downloadable purchase
|
|
15
|
+
confirmations, and email-verified licence claiming. Checkout requires configured
|
|
16
|
+
transactional email delivery.
|
|
17
|
+
|
|
18
|
+
### Improved
|
|
19
|
+
|
|
20
|
+
- Agent chat uses a shared centred width for messages and the composer, compact
|
|
21
|
+
conversation rows with avatars and unread counts, expandable tool details,
|
|
22
|
+
and clearer loading, empty, streaming, and agent-context displays.
|
|
23
|
+
- Default control backgrounds use the canvas colour. The command palette has
|
|
24
|
+
updated translucency and theme-aware shadows.
|
|
25
|
+
- The homepage includes interactive workspace, agent-chat, analytics, and designer
|
|
26
|
+
demos. Website pricing and checkout use consistent USD licence offers.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Calendar date numbers keep consistent spacing across all today-indicator modes,
|
|
31
|
+
and week numbers align with dates in the desktop grid.
|
|
32
|
+
- Website checkout validates the configured price and licence before redirecting
|
|
33
|
+
to payment. Delayed payment handling and repeat webhook processing preserve
|
|
34
|
+
the recorded licence and its fulfilment state.
|
|
35
|
+
|
|
36
|
+
### Upgrade notes
|
|
37
|
+
|
|
38
|
+
- Existing calendar indicator values remain supported; `cell` is still the default.
|
|
39
|
+
- `DomAgentConversation.maxMessageWidth` now controls the centred message column
|
|
40
|
+
and composer together. Review custom conversation layouts after upgrading.
|
|
41
|
+
- Applications that supply their own theme can set `--skin-control-bg` to retain
|
|
42
|
+
a different control background.
|
|
43
|
+
|
|
3
44
|
## 0.2.1 - 2026-09-21
|
|
4
45
|
|
|
5
46
|
Vue Router 5 support and a public home for DOM Studio release notes.
|
package/LICENSE.md
CHANGED
|
@@ -28,11 +28,14 @@ the order confirmation.
|
|
|
28
28
|
An Individual licence permits one named individual to:
|
|
29
29
|
|
|
30
30
|
- use and modify the Software in unlimited personal or commercial products owned by that individual;
|
|
31
|
+
- incorporate the Software into bespoke products created for that individual's clients;
|
|
31
32
|
- deploy and distribute those products to end users; and
|
|
32
33
|
- maintain those products for their end users.
|
|
33
34
|
|
|
34
|
-
The Individual licence
|
|
35
|
-
access with colleagues, employees,
|
|
35
|
+
The Individual licence covers one named developer. It does not permit sharing
|
|
36
|
+
the component library or paid source access with colleagues, employees, or
|
|
37
|
+
contractors. A client may receive the Software as part of a bespoke end product
|
|
38
|
+
under the client-delivery terms below.
|
|
36
39
|
|
|
37
40
|
### Team
|
|
38
41
|
|
|
@@ -44,9 +47,14 @@ A Team licence permits the purchasing legal entity to:
|
|
|
44
47
|
- incorporate the Software into bespoke products created for its clients.
|
|
45
48
|
|
|
46
49
|
Contractors may use the Software only while working on behalf of the purchasing
|
|
47
|
-
entity and must not retain or reuse it for unrelated work.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
entity and must not retain or reuse it for unrelated work.
|
|
51
|
+
|
|
52
|
+
### Client delivery
|
|
53
|
+
|
|
54
|
+
Under either commercial licence, a client may receive the Software only as part
|
|
55
|
+
of a bespoke end product. The client may operate and maintain that end product,
|
|
56
|
+
but may not extract or reuse the Software as a standalone component library or
|
|
57
|
+
in another product unless the client obtains its own licence.
|
|
50
58
|
|
|
51
59
|
## Permitted modifications and distribution
|
|
52
60
|
|
|
@@ -69,10 +77,16 @@ You must not:
|
|
|
69
77
|
|
|
70
78
|
## Updates
|
|
71
79
|
|
|
72
|
-
A
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
A commercial licence permits perpetual use of the versions obtained under it,
|
|
81
|
+
subject to the terms of this licence. Included updates cover releases published
|
|
82
|
+
under major version 1 while DOM Studio makes them available. Future major
|
|
83
|
+
upgrades are optional and may be sold separately; declining an upgrade does not
|
|
84
|
+
end the right to use the versions already obtained.
|
|
85
|
+
|
|
86
|
+
Hosted services, architecture reviews, consulting, and support are not included
|
|
87
|
+
unless the order confirmation says otherwise. Existing purchases, including
|
|
88
|
+
introductory £1 purchases and purchases described as "lifetime v1 access", retain
|
|
89
|
+
their purchased access and any additional rights or services promised at purchase.
|
|
76
90
|
|
|
77
91
|
## Ownership
|
|
78
92
|
|
package/package.json
CHANGED
|
@@ -1778,7 +1778,7 @@ export const componentCatalog = [
|
|
|
1778
1778
|
},
|
|
1779
1779
|
"name": "Agent conversation list",
|
|
1780
1780
|
"tag": "<DomAgentConversationList>",
|
|
1781
|
-
"description": "A reusable AI conversation navigation list with search, agent labels, unread counts, status
|
|
1781
|
+
"description": "A reusable AI conversation navigation list with search, agent labels, unread counts, status indicators, and active-thread selection.",
|
|
1782
1782
|
"icon": "M5 6h14M5 12h14M5 18h8",
|
|
1783
1783
|
"events": [
|
|
1784
1784
|
{
|
|
@@ -439,12 +439,17 @@ function matchesShortcut(event, shortcut) {
|
|
|
439
439
|
.command-panel-blur {
|
|
440
440
|
--skin-overlay-bg: var(
|
|
441
441
|
--dom-command-palette-background,
|
|
442
|
-
color-mix(in oklch, var(--canvas)
|
|
442
|
+
color-mix(in oklch, var(--canvas) 68%, transparent)
|
|
443
443
|
);
|
|
444
444
|
--skin-overlay-backdrop: var(
|
|
445
445
|
--dom-command-palette-backdrop,
|
|
446
446
|
blur(20px) saturate(1.12)
|
|
447
447
|
);
|
|
448
|
+
--skin-overlay-shadow: var(
|
|
449
|
+
--dom-command-palette-shadow,
|
|
450
|
+
0 40px 100px -24px rgb(15 23 42 / 0.3),
|
|
451
|
+
0 16px 40px -20px rgb(15 23 42 / 0.2)
|
|
452
|
+
);
|
|
448
453
|
}
|
|
449
454
|
|
|
450
455
|
.command-panel-entering {
|
|
@@ -103,6 +103,8 @@ export const agentChatShellProps = {
|
|
|
103
103
|
import { computed, useId } from 'vue';
|
|
104
104
|
import DomIconButton from '../../components/icon-button/DomIconButton.vue';
|
|
105
105
|
import DomSplitterPanel from '../../components/splitter-panel/DomSplitterPanel.vue';
|
|
106
|
+
import DomAvatar from '../../visual/avatar/DomAvatar.vue';
|
|
107
|
+
import DomBadge from '../../visual/badge/DomBadge.vue';
|
|
106
108
|
import DomStatusPill from '../../visual/status-pill/DomStatusPill.vue';
|
|
107
109
|
import DomAgentConversation from './DomAgentConversation.vue';
|
|
108
110
|
import DomAgentConversationList from './DomAgentConversationList.vue';
|
|
@@ -364,7 +366,7 @@ function conversationStatusTone() {
|
|
|
364
366
|
<template>
|
|
365
367
|
<section
|
|
366
368
|
:id="shellId"
|
|
367
|
-
class="h-full min-h-0 w-full min-w-0 overflow-hidden rounded-
|
|
369
|
+
class="h-full min-h-0 w-full min-w-0 overflow-hidden rounded-xl border border-border bg-canvas text-canvas-fg shadow-sm"
|
|
368
370
|
:aria-label="title"
|
|
369
371
|
>
|
|
370
372
|
<div class="hidden h-full min-h-0 md:block">
|
|
@@ -376,7 +378,7 @@ function conversationStatusTone() {
|
|
|
376
378
|
:min-main="minConversation"
|
|
377
379
|
:min-end="asideMinimum"
|
|
378
380
|
:handle-size="8"
|
|
379
|
-
handle-class="
|
|
381
|
+
handle-class="relative before:pointer-events-none before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2 before:bg-border/70 before:transition hover:before:bg-ring/60 data-[dragging=true]:before:bg-ring"
|
|
380
382
|
@update:start-size="updateSidebarSize"
|
|
381
383
|
@update:end-size="updateAsideSize"
|
|
382
384
|
>
|
|
@@ -440,70 +442,67 @@ function conversationStatusTone() {
|
|
|
440
442
|
<template v-if="asideOpen" #end>
|
|
441
443
|
<slot name="aside" :conversation="conversation" :active-agent="activeAgent" :tool-parts="toolParts">
|
|
442
444
|
<aside class="flex h-full min-h-0 flex-col bg-canvas">
|
|
443
|
-
<header class="shrink-0 border-b border-border
|
|
444
|
-
<div class="flex
|
|
445
|
+
<header class="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-border/70 px-4">
|
|
446
|
+
<div class="flex min-w-0 items-center gap-2.5">
|
|
447
|
+
<DomAvatar size="sm" shape="rounded" :src="activeAgent?.avatarUrl || ''" :name="activeAgentLabel()" />
|
|
445
448
|
<div class="min-w-0">
|
|
446
|
-
<
|
|
447
|
-
<
|
|
449
|
+
<h2 class="truncate text-sm font-semibold tracking-tight text-canvas-fg">{{ activeAgentLabel() }}</h2>
|
|
450
|
+
<p class="truncate font-mono text-[11px] text-muted-fg">{{ activeModelLabel() }}</p>
|
|
448
451
|
</div>
|
|
449
|
-
<DomIconButton label="Close context panel" size="sm" variant="ghost" icon="M6 6l12 12M18 6 6 18" @click="setAsideOpen(false)" />
|
|
450
452
|
</div>
|
|
453
|
+
<DomIconButton label="Close context panel" size="sm" variant="ghost" icon="M6 6l12 12M18 6 6 18" @click="setAsideOpen(false)" />
|
|
451
454
|
</header>
|
|
452
|
-
<div class="min-h-0 flex-1 overflow-y-auto
|
|
453
|
-
<
|
|
454
|
-
<p class="text-
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<p class="text-xs font-semibold uppercase text-muted-fg">Conversation</p>
|
|
462
|
-
<DomStatusPill size="sm" :tone="conversationStatusTone()">{{ streaming ? 'streaming' : (conversation?.status || 'idle') }}</DomStatusPill>
|
|
463
|
-
</div>
|
|
464
|
-
<dl class="mt-3 grid grid-cols-2 gap-3 text-sm">
|
|
465
|
-
<div>
|
|
466
|
-
<dt class="text-xs text-muted-fg">Messages</dt>
|
|
467
|
-
<dd class="font-semibold text-canvas-fg">{{ messages.length }}</dd>
|
|
455
|
+
<div class="min-h-0 flex-1 divide-y divide-border/70 overflow-y-auto">
|
|
456
|
+
<section class="p-4">
|
|
457
|
+
<p class="text-sm leading-6 text-muted-fg">{{ activeAgent?.role || 'Connect an agent to show instructions, tools, and memory context here.' }}</p>
|
|
458
|
+
<dl class="mt-4 grid grid-cols-3 gap-2">
|
|
459
|
+
<div class="rounded-lg bg-secondary/60 px-3 py-2">
|
|
460
|
+
<dt class="text-[11px] text-muted-fg">Status</dt>
|
|
461
|
+
<dd class="mt-1">
|
|
462
|
+
<DomStatusPill class="capitalize" size="sm" :tone="conversationStatusTone()" :pulse="streaming">{{ streaming ? 'streaming' : (conversation?.status || 'idle') }}</DomStatusPill>
|
|
463
|
+
</dd>
|
|
468
464
|
</div>
|
|
469
|
-
<div>
|
|
470
|
-
<dt class="text-
|
|
471
|
-
<dd class="font-semibold text-canvas-fg">{{
|
|
465
|
+
<div class="rounded-lg bg-secondary/60 px-3 py-2">
|
|
466
|
+
<dt class="text-[11px] text-muted-fg">Messages</dt>
|
|
467
|
+
<dd class="mt-0.5 text-base font-semibold tabular-nums text-canvas-fg">{{ messages.length }}</dd>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="rounded-lg bg-secondary/60 px-3 py-2">
|
|
470
|
+
<dt class="text-[11px] text-muted-fg">Tools</dt>
|
|
471
|
+
<dd class="mt-0.5 text-base font-semibold tabular-nums text-canvas-fg">{{ toolParts.length }}</dd>
|
|
472
472
|
</div>
|
|
473
473
|
</dl>
|
|
474
|
-
</
|
|
474
|
+
</section>
|
|
475
475
|
|
|
476
|
-
<
|
|
477
|
-
<
|
|
478
|
-
<
|
|
479
|
-
<
|
|
476
|
+
<section class="p-4">
|
|
477
|
+
<h3 class="text-xs font-medium uppercase tracking-wide text-muted-fg">Tool calls</h3>
|
|
478
|
+
<ul v-if="toolParts.length" class="mt-2 -mx-2">
|
|
479
|
+
<li
|
|
480
480
|
v-for="tool in toolParts"
|
|
481
481
|
:key="`${tool.messageId}-${tool.id || tool.name}`"
|
|
482
|
-
class="rounded-
|
|
482
|
+
class="flex items-center gap-3 rounded-lg px-2 py-2 transition hover:bg-secondary/50"
|
|
483
483
|
>
|
|
484
|
-
<
|
|
485
|
-
<
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
484
|
+
<span class="grid size-7 shrink-0 place-items-center rounded-lg bg-secondary text-muted-fg">
|
|
485
|
+
<svg viewBox="0 0 24 24" class="size-3.5" fill="none" aria-hidden="true">
|
|
486
|
+
<path d="M14.7 6.3a4 4 0 0 0-5.4 5.2L4 16.8V20h3.2l5.3-5.3a4 4 0 0 0 5.2-5.4l-2.5 2.5-2.3-.7-.7-2.3 2.5-2.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" />
|
|
487
|
+
</svg>
|
|
488
|
+
</span>
|
|
489
|
+
<span class="min-w-0 flex-1">
|
|
490
|
+
<span class="block truncate text-sm font-medium text-canvas-fg">{{ tool.label || tool.name }}</span>
|
|
491
|
+
<span class="block truncate font-mono text-[11px] text-muted-fg">{{ tool.name }}</span>
|
|
492
|
+
</span>
|
|
493
|
+
<span class="shrink-0 text-xs capitalize text-muted-fg">{{ tool.status || 'ready' }}</span>
|
|
494
|
+
</li>
|
|
495
|
+
</ul>
|
|
496
|
+
<p v-else class="mt-2 text-sm leading-6 text-muted-fg">Tool activity appears here as the agent works.</p>
|
|
497
|
+
</section>
|
|
493
498
|
|
|
494
|
-
<
|
|
495
|
-
<
|
|
496
|
-
<div v-if="activeAgent?.tools?.length" class="mt-3 flex flex-wrap gap-
|
|
497
|
-
<
|
|
498
|
-
v-for="tool in activeAgent.tools"
|
|
499
|
-
:key="tool"
|
|
500
|
-
class="rounded-full border border-border bg-secondary px-2.5 py-1 text-xs font-medium text-muted-fg"
|
|
501
|
-
>
|
|
502
|
-
{{ tool }}
|
|
503
|
-
</span>
|
|
499
|
+
<section class="p-4">
|
|
500
|
+
<h3 class="text-xs font-medium uppercase tracking-wide text-muted-fg">Available tools</h3>
|
|
501
|
+
<div v-if="activeAgent?.tools?.length" class="mt-3 flex flex-wrap gap-1.5">
|
|
502
|
+
<DomBadge v-for="tool in activeAgent.tools" :key="tool" class="font-mono" size="sm">{{ tool }}</DomBadge>
|
|
504
503
|
</div>
|
|
505
|
-
<p v-else class="mt-
|
|
506
|
-
</
|
|
504
|
+
<p v-else class="mt-2 text-sm leading-6 text-muted-fg">No tool manifest supplied.</p>
|
|
505
|
+
</section>
|
|
507
506
|
</div>
|
|
508
507
|
</aside>
|
|
509
508
|
</slot>
|
|
@@ -127,7 +127,7 @@ export const agentConversationProps = {
|
|
|
127
127
|
maxMessageWidth: {
|
|
128
128
|
type: String,
|
|
129
129
|
default: '48rem',
|
|
130
|
-
_edit: { description: 'CSS max-width applied to message
|
|
130
|
+
_edit: { description: 'CSS max-width applied to the centered message column and composer.' },
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
133
|
</script>
|
|
@@ -136,6 +136,10 @@ export const agentConversationProps = {
|
|
|
136
136
|
import { computed, nextTick, ref, useId, watch } from 'vue';
|
|
137
137
|
import DomIconButton from '../../components/icon-button/DomIconButton.vue';
|
|
138
138
|
import DomTextareaComposer from '../../forms/textarea-composer/DomTextareaComposer.vue';
|
|
139
|
+
import DomToggleButtonGroup from '../../forms/toggle-button-group/DomToggleButtonGroup.vue';
|
|
140
|
+
import DomAvatar from '../../visual/avatar/DomAvatar.vue';
|
|
141
|
+
import DomEmptyState from '../../visual/empty-state/DomEmptyState.vue';
|
|
142
|
+
import DomSkeleton from '../../visual/skeleton/DomSkeleton.vue';
|
|
139
143
|
import DomStatusPill from '../../visual/status-pill/DomStatusPill.vue';
|
|
140
144
|
|
|
141
145
|
defineOptions({
|
|
@@ -181,6 +185,11 @@ const emit = defineEmits([
|
|
|
181
185
|
'scroll-bottom',
|
|
182
186
|
]);
|
|
183
187
|
|
|
188
|
+
const modeOptions = [
|
|
189
|
+
{ label: 'Text', value: 'text' },
|
|
190
|
+
{ label: 'Audio', value: 'audio' },
|
|
191
|
+
];
|
|
192
|
+
|
|
184
193
|
const scroller = ref(null);
|
|
185
194
|
const isPinnedToBottom = ref(true);
|
|
186
195
|
const titleId = useId();
|
|
@@ -192,7 +201,7 @@ const statusTone = computed(() => statusToneFor(props.streaming ? 'streaming' :
|
|
|
192
201
|
const visibleMessages = computed(() => props.messages.filter(isVisibleMessage));
|
|
193
202
|
const canSubmit = computed(() => !props.disabled && !props.streaming && props.modelValue.trim().length > 0);
|
|
194
203
|
const composerPlaceholder = computed(() => props.mode === 'audio' ? 'Describe what you want to say, or connect this to speech input...' : props.placeholder);
|
|
195
|
-
const
|
|
204
|
+
const columnStyle = computed(() => ({ maxWidth: props.maxMessageWidth }));
|
|
196
205
|
const shouldShowJumpButton = computed(() => !isPinnedToBottom.value && visibleMessages.value.length > 2);
|
|
197
206
|
|
|
198
207
|
watch(() => props.conversation?.id, async () => {
|
|
@@ -351,22 +360,6 @@ function authorName(message) {
|
|
|
351
360
|
return findAgentById(props.agents, message.agentId)?.name || activeAgent.value?.name || 'Assistant';
|
|
352
361
|
}
|
|
353
362
|
|
|
354
|
-
/**
|
|
355
|
-
* Resolves the short avatar label for a message.
|
|
356
|
-
*
|
|
357
|
-
* @param {AgentChatMessage} message Message record.
|
|
358
|
-
* @returns {string} Avatar initials.
|
|
359
|
-
*/
|
|
360
|
-
function avatarLabel(message) {
|
|
361
|
-
return authorName(message)
|
|
362
|
-
.split(/\s+/)
|
|
363
|
-
.filter(Boolean)
|
|
364
|
-
.slice(0, 2)
|
|
365
|
-
.map(firstCharacter)
|
|
366
|
-
.join('')
|
|
367
|
-
.toUpperCase() || 'AI';
|
|
368
|
-
}
|
|
369
|
-
|
|
370
363
|
/**
|
|
371
364
|
* Resolves the timestamp label for a message.
|
|
372
365
|
*
|
|
@@ -417,19 +410,27 @@ function toolStatusLabel(status) {
|
|
|
417
410
|
}
|
|
418
411
|
|
|
419
412
|
/**
|
|
420
|
-
* Returns status
|
|
413
|
+
* Returns the status pill tone for a tool part.
|
|
421
414
|
*
|
|
422
415
|
* @param {string} status Tool status.
|
|
423
|
-
* @returns {
|
|
416
|
+
* @returns {'neutral'|'info'|'success'|'danger'} Status pill tone.
|
|
424
417
|
*/
|
|
425
|
-
function
|
|
418
|
+
function toolStatusTone(status) {
|
|
426
419
|
return {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
420
|
+
running: 'info',
|
|
421
|
+
complete: 'success',
|
|
422
|
+
error: 'danger',
|
|
423
|
+
}[status] || 'neutral';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Resolves the avatar image for a message author.
|
|
428
|
+
*
|
|
429
|
+
* @param {AgentChatMessage} message Message record.
|
|
430
|
+
* @returns {string} Avatar URL.
|
|
431
|
+
*/
|
|
432
|
+
function avatarUrl(message) {
|
|
433
|
+
return (findAgentById(props.agents, message.agentId) || activeAgent.value)?.avatarUrl || '';
|
|
433
434
|
}
|
|
434
435
|
|
|
435
436
|
/**
|
|
@@ -489,139 +490,138 @@ function statusToneFor(status) {
|
|
|
489
490
|
function trimText(value) {
|
|
490
491
|
return String(value || '').trim();
|
|
491
492
|
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Returns the first character from a word.
|
|
495
|
-
*
|
|
496
|
-
* @param {string} value Source word.
|
|
497
|
-
* @returns {string} First character.
|
|
498
|
-
*/
|
|
499
|
-
function firstCharacter(value) {
|
|
500
|
-
return value.charAt(0);
|
|
501
|
-
}
|
|
502
493
|
</script>
|
|
503
494
|
|
|
495
|
+
|
|
504
496
|
<template>
|
|
505
497
|
<section
|
|
506
|
-
class="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden rounded-
|
|
498
|
+
class="flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden rounded-xl border border-border bg-canvas text-canvas-fg shadow-sm"
|
|
507
499
|
:aria-labelledby="titleId"
|
|
508
500
|
>
|
|
509
|
-
<header v-if="showHeader" class="flex
|
|
501
|
+
<header v-if="showHeader" class="flex h-14 shrink-0 items-center gap-2 border-b border-border/70 px-3 sm:px-4">
|
|
510
502
|
<slot name="header-leading" :conversation="conversation" :active-agent="activeAgent" />
|
|
511
503
|
<div class="min-w-0 flex-1">
|
|
512
504
|
<div class="flex min-w-0 items-center gap-2">
|
|
513
|
-
<h2 :id="titleId" class="truncate text-sm font-semibold text-canvas-fg">{{ conversationTitle }}</h2>
|
|
514
|
-
<DomStatusPill class="hidden sm:inline-flex" size="sm" :tone="statusTone">{{ streaming ? 'Streaming' : (conversation?.status || 'Idle') }}</DomStatusPill>
|
|
505
|
+
<h2 :id="titleId" class="truncate text-sm font-semibold tracking-tight text-canvas-fg">{{ conversationTitle }}</h2>
|
|
506
|
+
<DomStatusPill class="hidden shrink-0 capitalize sm:inline-flex" size="sm" :tone="statusTone" :pulse="streaming">{{ streaming ? 'Streaming' : (conversation?.status || 'Idle') }}</DomStatusPill>
|
|
515
507
|
</div>
|
|
516
|
-
<p class="
|
|
508
|
+
<p class="truncate text-xs text-muted-fg">{{ conversationSummary }}</p>
|
|
517
509
|
</div>
|
|
518
|
-
<div class="flex shrink-0 items-center gap-1">
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
<button
|
|
529
|
-
type="button"
|
|
530
|
-
class="rounded-full px-2.5 py-1 text-xs font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
531
|
-
:class="mode === 'audio' ? 'bg-primary text-primary-fg' : 'text-muted-fg hover:bg-secondary hover:text-canvas-fg'"
|
|
532
|
-
:aria-pressed="mode === 'audio'"
|
|
533
|
-
@click="setMode('audio')"
|
|
534
|
-
>
|
|
535
|
-
Audio
|
|
536
|
-
</button>
|
|
510
|
+
<div class="flex shrink-0 items-center gap-1.5">
|
|
511
|
+
<DomToggleButtonGroup
|
|
512
|
+
:model-value="mode"
|
|
513
|
+
:options="modeOptions"
|
|
514
|
+
:chrome="false"
|
|
515
|
+
label="Input mode"
|
|
516
|
+
size="sm"
|
|
517
|
+
variant="switch"
|
|
518
|
+
@update:model-value="setMode"
|
|
519
|
+
/>
|
|
537
520
|
<slot name="header-actions" :conversation="conversation" :active-agent="activeAgent" :streaming="streaming" />
|
|
538
521
|
</div>
|
|
539
522
|
</header>
|
|
540
523
|
|
|
541
524
|
<div
|
|
542
525
|
ref="scroller"
|
|
543
|
-
class="min-h-0 flex-1 overflow-y-auto
|
|
526
|
+
class="min-h-0 flex-1 overflow-y-auto"
|
|
544
527
|
@scroll.passive="updateScrollState"
|
|
545
528
|
>
|
|
546
|
-
<div
|
|
547
|
-
<div v-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<div class="mx-auto grid size-12 place-items-center rounded-full bg-secondary text-muted-fg">
|
|
554
|
-
<svg viewBox="0 0 24 24" class="size-5" fill="none" aria-hidden="true">
|
|
555
|
-
<path d="M5 7h14M5 12h10M5 17h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
556
|
-
</svg>
|
|
529
|
+
<div class="mx-auto flex min-h-full w-full flex-col px-4 py-6 sm:px-6" :style="columnStyle">
|
|
530
|
+
<div v-if="loading" class="mt-auto flex flex-col gap-8">
|
|
531
|
+
<div v-for="index in 2" :key="index" class="flex flex-col gap-8">
|
|
532
|
+
<DomSkeleton class="ml-auto h-10 w-2/5 rounded-2xl" />
|
|
533
|
+
<div class="flex gap-3">
|
|
534
|
+
<DomSkeleton variant="circle" width="2rem" height="2rem" class="shrink-0" />
|
|
535
|
+
<DomSkeleton class="flex-1" variant="text" :lines="3" />
|
|
557
536
|
</div>
|
|
558
|
-
<p class="mt-3 text-sm font-semibold text-canvas-fg">No messages yet</p>
|
|
559
|
-
<p class="mt-1 text-sm leading-6 text-muted-fg">{{ emptyText }}</p>
|
|
560
537
|
</div>
|
|
561
538
|
</div>
|
|
562
|
-
</slot>
|
|
563
539
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
540
|
+
<slot v-else-if="!visibleMessages.length" name="empty">
|
|
541
|
+
<div class="flex flex-1 items-center justify-center">
|
|
542
|
+
<DomEmptyState
|
|
543
|
+
class="w-full max-w-sm border-transparent bg-transparent"
|
|
544
|
+
size="sm"
|
|
545
|
+
title="No messages yet"
|
|
546
|
+
:description="emptyText"
|
|
547
|
+
icon="M7 8h10M7 12h6m-9 8 2.6-3H18a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v14Z"
|
|
548
|
+
/>
|
|
573
549
|
</div>
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
550
|
+
</slot>
|
|
551
|
+
|
|
552
|
+
<div v-else class="flex flex-col gap-7">
|
|
553
|
+
<article
|
|
554
|
+
v-for="message in visibleMessages"
|
|
555
|
+
:key="message.id"
|
|
556
|
+
class="flex min-w-0 gap-3"
|
|
557
|
+
:class="isUserMessage(message) ? 'justify-end' : 'justify-start'"
|
|
558
|
+
>
|
|
559
|
+
<DomAvatar
|
|
560
|
+
v-if="!isUserMessage(message)"
|
|
561
|
+
class="mt-0.5 hidden sm:inline-grid"
|
|
562
|
+
size="sm"
|
|
563
|
+
shape="rounded"
|
|
564
|
+
:src="avatarUrl(message)"
|
|
565
|
+
:name="authorName(message)"
|
|
566
|
+
/>
|
|
567
|
+
|
|
568
|
+
<slot name="message" :message="message" :active-agent="activeAgent" :parts="partsForMessage(message)">
|
|
581
569
|
<div
|
|
582
|
-
class="min-w-0
|
|
583
|
-
:class="isUserMessage(message)
|
|
584
|
-
? 'border-primary/20 bg-primary text-primary-fg'
|
|
585
|
-
: 'border-border bg-canvas'"
|
|
570
|
+
class="flex min-w-0 flex-col"
|
|
571
|
+
:class="isUserMessage(message) ? 'max-w-[85%] items-end' : 'flex-1'"
|
|
586
572
|
>
|
|
587
|
-
<div class="mb-
|
|
588
|
-
<span class="font-semibold">{{ authorName(message) }}</span>
|
|
589
|
-
<span v-if="messageTime(message)">{{ messageTime(message) }}</span>
|
|
573
|
+
<div v-if="!isUserMessage(message)" class="mb-1 flex h-6 items-center gap-2 text-xs">
|
|
574
|
+
<span class="font-semibold text-canvas-fg">{{ authorName(message) }}</span>
|
|
575
|
+
<span v-if="messageTime(message)" class="text-muted-fg">{{ messageTime(message) }}</span>
|
|
590
576
|
</div>
|
|
591
577
|
|
|
592
|
-
<div
|
|
578
|
+
<div
|
|
579
|
+
class="min-w-0 space-y-3 text-sm"
|
|
580
|
+
:class="isUserMessage(message)
|
|
581
|
+
? 'rounded-2xl rounded-br-md bg-secondary px-4 py-2.5 leading-6 text-secondary-fg'
|
|
582
|
+
: 'leading-7 text-canvas-fg'"
|
|
583
|
+
>
|
|
593
584
|
<template v-for="(part, index) in partsForMessage(message)" :key="part.id || `${message.id}-${index}`">
|
|
594
|
-
<div v-if="part.type === 'text'" class="space-y-
|
|
585
|
+
<div v-if="part.type === 'text'" class="space-y-3">
|
|
595
586
|
<p v-for="paragraph in textParagraphs(part.text)" :key="paragraph" class="whitespace-pre-wrap">{{ paragraph }}</p>
|
|
596
587
|
</div>
|
|
597
588
|
|
|
598
589
|
<details
|
|
599
590
|
v-else-if="part.type === 'reasoning' && showReasoning"
|
|
600
|
-
class="
|
|
591
|
+
class="group/reasoning"
|
|
601
592
|
:open="part.open"
|
|
602
593
|
>
|
|
603
|
-
<summary class="cursor-pointer text-xs font-
|
|
604
|
-
|
|
594
|
+
<summary class="inline-flex cursor-pointer list-none items-center gap-1.5 rounded-md text-xs font-medium text-muted-fg transition hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 [&::-webkit-details-marker]:hidden">
|
|
595
|
+
<svg viewBox="0 0 24 24" class="size-3.5" fill="none" aria-hidden="true">
|
|
596
|
+
<path d="M12 3v3m0 12v3M3 12h3m12 0h3M6.3 6.3l2.1 2.1m7.2 7.2 2.1 2.1m0-11.4-2.1 2.1m-7.2 7.2-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
597
|
+
</svg>
|
|
598
|
+
Reasoning
|
|
599
|
+
<svg viewBox="0 0 24 24" class="size-3.5 transition group-open/reasoning:rotate-90" fill="none" aria-hidden="true">
|
|
600
|
+
<path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
601
|
+
</svg>
|
|
602
|
+
</summary>
|
|
603
|
+
<p class="mt-2 whitespace-pre-wrap border-l-2 border-border pl-3 text-xs leading-5 text-muted-fg">{{ part.text }}</p>
|
|
605
604
|
</details>
|
|
606
605
|
|
|
607
|
-
<div v-else-if="part.type === 'audio'" class="
|
|
608
|
-
<
|
|
609
|
-
<
|
|
610
|
-
<
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
<
|
|
615
|
-
|
|
616
|
-
<
|
|
617
|
-
<span v-for="bar in 18" :key="bar" class="w-1 rounded-full bg-primary/45" :style="{ height: `${8 + ((bar * 7) % 18)}px` }"></span>
|
|
618
|
-
</div>
|
|
606
|
+
<div v-else-if="part.type === 'audio'" class="flex items-center gap-3 rounded-xl bg-secondary/70 p-2.5 pr-4 text-secondary-fg">
|
|
607
|
+
<span class="grid size-8 shrink-0 place-items-center rounded-full bg-primary text-primary-fg">
|
|
608
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
|
|
609
|
+
<path d="M12 4a3 3 0 0 0-3 3v5a3 3 0 1 0 6 0V7a3 3 0 0 0-3-3Zm-6 8a6 6 0 0 0 12 0m-6 6v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
610
|
+
</svg>
|
|
611
|
+
</span>
|
|
612
|
+
<div class="min-w-0 flex-1">
|
|
613
|
+
<p class="text-xs font-medium">{{ part.label || 'Audio message' }}</p>
|
|
614
|
+
<div class="mt-1 flex h-5 items-center gap-0.5" aria-hidden="true">
|
|
615
|
+
<span v-for="bar in 28" :key="bar" class="w-0.5 rounded-full bg-current opacity-40" :style="{ height: `${4 + ((bar * 7) % 16)}px` }"></span>
|
|
619
616
|
</div>
|
|
617
|
+
<p v-if="part.text" class="mt-1 text-xs leading-5 text-muted-fg">{{ part.text }}</p>
|
|
620
618
|
</div>
|
|
621
|
-
<p v-if="part.text" class="mt-2 text-xs leading-5 text-muted-fg">{{ part.text }}</p>
|
|
622
619
|
</div>
|
|
623
620
|
|
|
624
|
-
<div
|
|
621
|
+
<div
|
|
622
|
+
v-else-if="part.type === 'tool-call' || part.type === 'tool-result'"
|
|
623
|
+
class="overflow-hidden rounded-xl border border-border bg-canvas text-canvas-fg shadow-xs"
|
|
624
|
+
>
|
|
625
625
|
<slot
|
|
626
626
|
name="tool"
|
|
627
627
|
:tool="part"
|
|
@@ -637,50 +637,59 @@ function firstCharacter(value) {
|
|
|
637
637
|
:result="part.result"
|
|
638
638
|
@action="emitToolAction($event, part, message)"
|
|
639
639
|
/>
|
|
640
|
-
<
|
|
641
|
-
<
|
|
642
|
-
<
|
|
643
|
-
<
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
<span class="shrink-0 rounded-full border px-2 py-0.5 text-[11px] font-medium" :class="toolStatusClass(part.status)">
|
|
647
|
-
{{ toolStatusLabel(part.status) }}
|
|
640
|
+
<details v-else class="group/tool">
|
|
641
|
+
<summary class="flex cursor-pointer list-none items-center gap-3 px-3 py-2.5 transition hover:bg-secondary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50 [&::-webkit-details-marker]:hidden">
|
|
642
|
+
<span class="grid size-7 shrink-0 place-items-center rounded-lg bg-secondary text-muted-fg">
|
|
643
|
+
<svg viewBox="0 0 24 24" class="size-3.5" fill="none" aria-hidden="true">
|
|
644
|
+
<path d="M14.7 6.3a4 4 0 0 0-5.4 5.2L4 16.8V20h3.2l5.3-5.3a4 4 0 0 0 5.2-5.4l-2.5 2.5-2.3-.7-.7-2.3 2.5-2.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" />
|
|
645
|
+
</svg>
|
|
648
646
|
</span>
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
647
|
+
<span class="min-w-0 flex-1">
|
|
648
|
+
<span class="block truncate text-sm font-medium leading-5">{{ part.label || part.name || 'Tool call' }}</span>
|
|
649
|
+
<span class="block truncate font-mono text-[11px] leading-4 text-muted-fg">{{ part.name || 'tool' }}</span>
|
|
650
|
+
</span>
|
|
651
|
+
<DomStatusPill class="shrink-0" size="sm" :tone="toolStatusTone(part.status)" :pulse="part.status === 'running'">{{ toolStatusLabel(part.status) }}</DomStatusPill>
|
|
652
|
+
<svg viewBox="0 0 24 24" class="size-4 shrink-0 text-muted-fg transition group-open/tool:rotate-90" fill="none" aria-hidden="true">
|
|
653
|
+
<path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
654
|
+
</svg>
|
|
655
|
+
</summary>
|
|
656
|
+
<div class="grid gap-3 border-t border-border bg-secondary/30 p-3 md:grid-cols-2">
|
|
657
|
+
<div class="min-w-0">
|
|
658
|
+
<p class="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-fg">Arguments</p>
|
|
659
|
+
<pre class="max-h-40 overflow-auto rounded-lg bg-canvas p-2.5 font-mono text-xs leading-5 text-muted-fg ring-1 ring-border">{{ formatJson(part.args) }}</pre>
|
|
654
660
|
</div>
|
|
655
|
-
<div>
|
|
656
|
-
<p class="mb-1 text-[11px] font-
|
|
657
|
-
<pre class="max-h-40 overflow-auto rounded-
|
|
661
|
+
<div class="min-w-0">
|
|
662
|
+
<p class="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-fg">Result</p>
|
|
663
|
+
<pre class="max-h-40 overflow-auto rounded-lg bg-canvas p-2.5 font-mono text-xs leading-5 text-muted-fg ring-1 ring-border">{{ formatJson(part.result) }}</pre>
|
|
658
664
|
</div>
|
|
659
665
|
</div>
|
|
660
|
-
</
|
|
666
|
+
</details>
|
|
661
667
|
</slot>
|
|
662
668
|
</div>
|
|
663
669
|
|
|
664
|
-
<div v-else-if="part.type === 'handoff'" class="rounded-
|
|
665
|
-
<
|
|
666
|
-
|
|
670
|
+
<div v-else-if="part.type === 'handoff'" class="flex gap-2.5 rounded-xl border border-warning/30 bg-warning/10 px-3 py-2.5">
|
|
671
|
+
<svg viewBox="0 0 24 24" class="mt-0.5 size-4 shrink-0 text-warning" fill="none" aria-hidden="true">
|
|
672
|
+
<path d="M7 7h11m0 0-3-3m3 3-3 3M17 17H6m0 0 3-3m-3 3 3 3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
673
|
+
</svg>
|
|
674
|
+
<div class="min-w-0">
|
|
675
|
+
<p class="text-xs font-semibold text-canvas-fg">{{ part.label || 'Agent handoff' }}</p>
|
|
676
|
+
<p class="mt-0.5 text-xs leading-5 text-muted-fg">{{ part.text }}</p>
|
|
677
|
+
</div>
|
|
667
678
|
</div>
|
|
668
679
|
</template>
|
|
669
680
|
|
|
670
|
-
<div v-if="isStreamingMessage(message)" class="flex items-center gap-1
|
|
681
|
+
<div v-if="isStreamingMessage(message)" class="flex h-6 items-center gap-1 text-muted-fg" role="status" aria-label="Assistant is typing">
|
|
671
682
|
<span class="size-1.5 animate-pulse rounded-full bg-current"></span>
|
|
672
|
-
<span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:
|
|
673
|
-
<span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:
|
|
683
|
+
<span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:150ms]"></span>
|
|
684
|
+
<span class="size-1.5 animate-pulse rounded-full bg-current [animation-delay:300ms]"></span>
|
|
674
685
|
</div>
|
|
675
686
|
</div>
|
|
676
|
-
</div>
|
|
677
|
-
</div>
|
|
678
|
-
</slot>
|
|
679
687
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
688
|
+
<span v-if="isUserMessage(message) && messageTime(message)" class="mt-1 px-1 text-[11px] text-muted-fg">{{ messageTime(message) }}</span>
|
|
689
|
+
</div>
|
|
690
|
+
</slot>
|
|
691
|
+
</article>
|
|
692
|
+
</div>
|
|
684
693
|
</div>
|
|
685
694
|
</div>
|
|
686
695
|
|
|
@@ -695,16 +704,16 @@ function firstCharacter(value) {
|
|
|
695
704
|
/>
|
|
696
705
|
</div>
|
|
697
706
|
|
|
698
|
-
<footer v-if="showFooter" class="shrink-0
|
|
707
|
+
<footer v-if="showFooter" class="relative shrink-0 px-3 pb-3 sm:px-4 sm:pb-4 before:pointer-events-none before:absolute before:inset-x-0 before:bottom-full before:h-6 before:bg-linear-to-t before:from-canvas before:to-transparent">
|
|
699
708
|
<slot name="footer" :value="modelValue" :mode="mode" :submit="submitComposer" :set-mode="setMode">
|
|
700
|
-
<div>
|
|
709
|
+
<div class="mx-auto w-full [--skin-input-shadow:var(--shadow-md)] [&_.skin-input]:rounded-2xl" :style="columnStyle">
|
|
701
710
|
<DomTextareaComposer
|
|
702
711
|
:model-value="modelValue"
|
|
703
712
|
:placeholder="composerPlaceholder"
|
|
704
713
|
:disabled="disabled || streaming"
|
|
705
714
|
:submit-disabled="!canSubmit"
|
|
706
715
|
:show-submit="!streaming"
|
|
707
|
-
:rows="
|
|
716
|
+
:rows="1"
|
|
708
717
|
:max-rows="8"
|
|
709
718
|
:chrome="false"
|
|
710
719
|
@update:model-value="updateComposer"
|
|
@@ -727,9 +736,9 @@ function firstCharacter(value) {
|
|
|
727
736
|
/>
|
|
728
737
|
</template>
|
|
729
738
|
</DomTextareaComposer>
|
|
730
|
-
<div class="mt-2 flex items-center justify-between gap-2 px-
|
|
731
|
-
<span>{{ mode === 'audio' ? 'Audio handoff ready' : 'Enter to send
|
|
732
|
-
<span v-if="activeAgent">{{ activeAgent.model }}</span>
|
|
739
|
+
<div class="mt-2 flex items-center justify-between gap-2 px-2 text-[11px] text-muted-fg">
|
|
740
|
+
<span class="hidden truncate sm:inline">{{ mode === 'audio' ? 'Audio handoff ready' : 'Enter to send · Shift + Enter for a new line' }}</span>
|
|
741
|
+
<span v-if="activeAgent?.model" class="ml-auto shrink-0 whitespace-nowrap font-mono">{{ activeAgent.model }}</span>
|
|
733
742
|
</div>
|
|
734
743
|
</div>
|
|
735
744
|
</slot>
|
|
@@ -46,14 +46,15 @@ export const agentConversationListProps = {
|
|
|
46
46
|
<script setup>
|
|
47
47
|
import { computed, useId } from 'vue';
|
|
48
48
|
import DomIconButton from '../../components/icon-button/DomIconButton.vue';
|
|
49
|
-
import
|
|
49
|
+
import DomAvatar from '../../visual/avatar/DomAvatar.vue';
|
|
50
|
+
import DomSkeleton from '../../visual/skeleton/DomSkeleton.vue';
|
|
50
51
|
|
|
51
52
|
defineOptions({
|
|
52
53
|
__doc: {
|
|
53
54
|
nav: { section: 'ai' },
|
|
54
55
|
name: 'Agent conversation list',
|
|
55
56
|
tag: '<DomAgentConversationList>',
|
|
56
|
-
description: 'A reusable AI conversation navigation list with search, agent labels, unread counts, status
|
|
57
|
+
description: 'A reusable AI conversation navigation list with search, agent labels, unread counts, status indicators, and active-thread selection.',
|
|
57
58
|
icon: 'M5 6h14M5 12h14M5 18h8',
|
|
58
59
|
events: [
|
|
59
60
|
{ name: 'update:search', payload: 'string', description: 'Fired when the sidebar search changes.' },
|
|
@@ -148,20 +149,13 @@ function conversationSummary(conversation) {
|
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
/**
|
|
151
|
-
* Returns
|
|
152
|
+
* Returns the agent name used for a conversation avatar.
|
|
152
153
|
*
|
|
153
154
|
* @param {Record<string, unknown>} conversation Conversation summary.
|
|
154
|
-
* @returns {string}
|
|
155
|
+
* @returns {string} Agent name.
|
|
155
156
|
*/
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
return name
|
|
159
|
-
.split(/\s+/)
|
|
160
|
-
.filter(Boolean)
|
|
161
|
-
.slice(0, 2)
|
|
162
|
-
.map(firstCharacter)
|
|
163
|
-
.join('')
|
|
164
|
-
.toUpperCase() || 'AI';
|
|
157
|
+
function agentName(conversation) {
|
|
158
|
+
return agentForConversation(conversation)?.name || 'AI';
|
|
165
159
|
}
|
|
166
160
|
|
|
167
161
|
/**
|
|
@@ -182,18 +176,18 @@ function updatedLabel(conversation) {
|
|
|
182
176
|
}
|
|
183
177
|
|
|
184
178
|
/**
|
|
185
|
-
* Resolves
|
|
179
|
+
* Resolves the status dot class for a conversation.
|
|
186
180
|
*
|
|
187
181
|
* @param {Record<string, unknown>} conversation Conversation summary.
|
|
188
|
-
* @returns {
|
|
182
|
+
* @returns {string} Token-based dot classes.
|
|
189
183
|
*/
|
|
190
|
-
function
|
|
184
|
+
function statusDotClass(conversation) {
|
|
191
185
|
return {
|
|
192
|
-
streaming: '
|
|
193
|
-
waiting: 'warning',
|
|
194
|
-
error: '
|
|
195
|
-
complete: 'success',
|
|
196
|
-
}[conversation.status] || '
|
|
186
|
+
streaming: 'bg-primary animate-pulse',
|
|
187
|
+
waiting: 'bg-warning',
|
|
188
|
+
error: 'bg-destructive',
|
|
189
|
+
complete: 'bg-success',
|
|
190
|
+
}[conversation.status] || 'bg-muted-fg/50';
|
|
197
191
|
}
|
|
198
192
|
|
|
199
193
|
/**
|
|
@@ -218,37 +212,27 @@ function conversationMatchesQuery(conversation, query) {
|
|
|
218
212
|
.toLowerCase()
|
|
219
213
|
.includes(query);
|
|
220
214
|
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Returns the first character from a word.
|
|
224
|
-
*
|
|
225
|
-
* @param {string} value Source word.
|
|
226
|
-
* @returns {string} First character.
|
|
227
|
-
*/
|
|
228
|
-
function firstCharacter(value) {
|
|
229
|
-
return value.charAt(0);
|
|
230
|
-
}
|
|
231
215
|
</script>
|
|
232
216
|
|
|
233
217
|
<template>
|
|
234
218
|
<aside class="flex h-full min-h-0 w-full min-w-0 flex-col bg-canvas text-canvas-fg">
|
|
235
|
-
<header class="shrink-0
|
|
236
|
-
<div class="flex items-center justify-between gap-3">
|
|
237
|
-
<h2 class="truncate text-sm font-semibold text-canvas-fg">{{ title }}</h2>
|
|
219
|
+
<header class="shrink-0 px-3 pb-2">
|
|
220
|
+
<div class="flex h-14 items-center justify-between gap-3 px-1">
|
|
221
|
+
<h2 class="truncate text-sm font-semibold tracking-tight text-canvas-fg">{{ title }}</h2>
|
|
238
222
|
<div class="flex shrink-0 items-center gap-1">
|
|
239
223
|
<slot name="actions" :conversations="conversations" />
|
|
240
224
|
<DomIconButton
|
|
241
225
|
v-if="showNewButton"
|
|
242
226
|
label="New conversation"
|
|
243
227
|
size="sm"
|
|
244
|
-
variant="
|
|
228
|
+
variant="ghost"
|
|
245
229
|
icon="M12 5v14M5 12h14"
|
|
246
230
|
@click="createConversation"
|
|
247
231
|
/>
|
|
248
232
|
</div>
|
|
249
233
|
</div>
|
|
250
234
|
<label :for="searchId" class="sr-only">Search conversations</label>
|
|
251
|
-
<div class="
|
|
235
|
+
<div class="skin-input flex items-center gap-2 px-2.5 py-1.5 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30">
|
|
252
236
|
<svg viewBox="0 0 24 24" class="size-4 shrink-0 text-muted-fg" fill="none" aria-hidden="true">
|
|
253
237
|
<path d="M11 5a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm4.5 10.5L20 20" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
|
254
238
|
</svg>
|
|
@@ -263,18 +247,19 @@ function firstCharacter(value) {
|
|
|
263
247
|
</div>
|
|
264
248
|
</header>
|
|
265
249
|
|
|
266
|
-
<div class="min-h-0 flex-1 overflow-y-auto
|
|
267
|
-
<div v-if="loading" class="space-y-
|
|
268
|
-
<div v-for="index in 5" :key="index" class="
|
|
250
|
+
<div class="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
|
251
|
+
<div v-if="loading" class="space-y-1">
|
|
252
|
+
<div v-for="index in 5" :key="index" class="flex gap-2.5 px-2.5 py-2.5">
|
|
253
|
+
<DomSkeleton variant="circle" width="2rem" height="2rem" class="shrink-0" />
|
|
254
|
+
<DomSkeleton class="flex-1" variant="text" :lines="2" />
|
|
255
|
+
</div>
|
|
269
256
|
</div>
|
|
270
257
|
|
|
271
258
|
<slot v-else-if="!filteredConversations.length" name="empty">
|
|
272
|
-
<
|
|
273
|
-
<p class="text-sm leading-6 text-muted-fg">{{ emptyText }}</p>
|
|
274
|
-
</div>
|
|
259
|
+
<p class="px-3 py-8 text-center text-sm leading-6 text-muted-fg">{{ emptyText }}</p>
|
|
275
260
|
</slot>
|
|
276
261
|
|
|
277
|
-
<div v-else class="space-y-
|
|
262
|
+
<div v-else class="space-y-0.5">
|
|
278
263
|
<slot
|
|
279
264
|
v-for="conversation in filteredConversations"
|
|
280
265
|
:key="conversation.id"
|
|
@@ -285,27 +270,38 @@ function firstCharacter(value) {
|
|
|
285
270
|
>
|
|
286
271
|
<button
|
|
287
272
|
type="button"
|
|
288
|
-
class="w-full rounded-lg
|
|
289
|
-
:class="isActiveConversation(conversation)
|
|
290
|
-
|
|
291
|
-
: 'border-transparent hover:border-border hover:bg-secondary/45'"
|
|
273
|
+
class="w-full rounded-lg px-2.5 py-2.5 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
274
|
+
:class="isActiveConversation(conversation) ? 'bg-secondary' : 'hover:bg-secondary/50'"
|
|
275
|
+
:aria-current="isActiveConversation(conversation) ? 'true' : undefined"
|
|
292
276
|
@click="selectConversation(conversation)"
|
|
293
277
|
>
|
|
294
|
-
<div class="flex items-start gap-
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
278
|
+
<div class="flex items-start gap-2.5">
|
|
279
|
+
<span class="relative shrink-0">
|
|
280
|
+
<DomAvatar
|
|
281
|
+
size="sm"
|
|
282
|
+
shape="rounded"
|
|
283
|
+
:src="agentForConversation(conversation)?.avatarUrl || ''"
|
|
284
|
+
:name="agentName(conversation)"
|
|
285
|
+
/>
|
|
286
|
+
<span
|
|
287
|
+
class="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full ring-2"
|
|
288
|
+
:class="[statusDotClass(conversation), isActiveConversation(conversation) ? 'ring-secondary' : 'ring-canvas']"
|
|
289
|
+
:title="conversation.status || 'idle'"
|
|
290
|
+
aria-hidden="true"
|
|
291
|
+
></span>
|
|
292
|
+
</span>
|
|
298
293
|
<div class="min-w-0 flex-1">
|
|
299
|
-
<div class="flex min-w-0 items-
|
|
300
|
-
<p class="truncate text-sm font-
|
|
294
|
+
<div class="flex min-w-0 items-baseline justify-between gap-2">
|
|
295
|
+
<p class="truncate text-sm font-medium text-canvas-fg">{{ conversationTitle(conversation) }}</p>
|
|
301
296
|
<span class="shrink-0 text-[11px] text-muted-fg">{{ updatedLabel(conversation) }}</span>
|
|
302
297
|
</div>
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
298
|
+
<div class="mt-0.5 flex min-w-0 items-center gap-2">
|
|
299
|
+
<p class="min-w-0 flex-1 truncate text-xs leading-5 text-muted-fg">
|
|
300
|
+
<span class="sr-only">{{ conversation.status || 'idle' }}. </span>{{ conversationSummary(conversation) }}
|
|
301
|
+
</p>
|
|
306
302
|
<span
|
|
307
303
|
v-if="conversation.unreadCount"
|
|
308
|
-
class="grid min-w-5 place-items-center rounded-full bg-primary px-1
|
|
304
|
+
class="grid h-4.5 min-w-4.5 shrink-0 place-items-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-fg"
|
|
309
305
|
>
|
|
310
306
|
{{ conversation.unreadCount }}
|
|
311
307
|
</span>
|
|
@@ -264,8 +264,8 @@ export const monthCalendarProps = {
|
|
|
264
264
|
* @param {string} value Requested indicator style.
|
|
265
265
|
* @returns {boolean} Whether the indicator is supported.
|
|
266
266
|
*/
|
|
267
|
-
validator: (value) => ['cell', 'circle', 'none'].includes(value),
|
|
268
|
-
_edit: { group: 'Styling', options: ['cell', 'circle', 'none'], description: 'Highlight today with a cell outline, a coloured circle behind the date, or no visual indicator.' },
|
|
267
|
+
validator: (value) => ['cell', 'circle', 'both', 'none'].includes(value),
|
|
268
|
+
_edit: { group: 'Styling', options: ['cell', 'circle', 'both', 'none'], description: 'Highlight today with a cell outline, a coloured circle behind the date, both, or no visual indicator.' },
|
|
269
269
|
},
|
|
270
270
|
/**
|
|
271
271
|
* @type {MonthCalendarDayClass}
|
|
@@ -273,7 +273,7 @@ export const monthCalendarProps = {
|
|
|
273
273
|
todayDateClass: {
|
|
274
274
|
type: [String, Array, Object, Function],
|
|
275
275
|
default: 'bg-primary text-primary-fg',
|
|
276
|
-
_edit: { group: 'Styling', description: 'Classes for today’s date circle. Replaces the default primary background and foreground colours when todayIndicator is circle.' },
|
|
276
|
+
_edit: { group: 'Styling', description: 'Classes for today’s date circle. Replaces the default primary background and foreground colours when todayIndicator is circle or both.' },
|
|
277
277
|
},
|
|
278
278
|
/**
|
|
279
279
|
* @type {MonthCalendarDayClass}
|
|
@@ -1632,7 +1632,7 @@ function onDayClick(cell, event) {
|
|
|
1632
1632
|
v-if="hasRowRail"
|
|
1633
1633
|
role="rowheader"
|
|
1634
1634
|
:aria-label="rowHeaderLabelFor(week)"
|
|
1635
|
-
class="dom-month-calendar__row-rail flex gap-3 px-3 py-
|
|
1635
|
+
class="dom-month-calendar__row-rail flex gap-3 px-3 py-[var(--dom-month-calendar-row-rail-padding)] text-xs font-semibold uppercase tracking-wide text-muted-fg"
|
|
1636
1636
|
:class="[
|
|
1637
1637
|
week.index > 0 && 'border-t border-border',
|
|
1638
1638
|
section.continuous ? 'bg-canvas' : 'bg-secondary/35',
|
|
@@ -1648,7 +1648,7 @@ function onDayClick(cell, event) {
|
|
|
1648
1648
|
{{ label }}
|
|
1649
1649
|
</span>
|
|
1650
1650
|
</div>
|
|
1651
|
-
<span v-if="showWeekNumbers" class="text-base leading-none tabular-nums">{{ week.weekNumber }}</span>
|
|
1651
|
+
<span v-if="showWeekNumbers" class="order-[var(--dom-month-calendar-week-number-order)] inline-flex size-7 shrink-0 items-center justify-center text-base leading-none tabular-nums">{{ week.weekNumber }}</span>
|
|
1652
1652
|
</div>
|
|
1653
1653
|
<component
|
|
1654
1654
|
:is="clickable ? 'button' : 'div'"
|
|
@@ -1681,7 +1681,7 @@ function onDayClick(cell, event) {
|
|
|
1681
1681
|
clickable && !cell.disabled && hoverableDays && 'hover:bg-secondary/50',
|
|
1682
1682
|
clickable && cell.disabled && 'cursor-not-allowed opacity-55',
|
|
1683
1683
|
!clickable && 'cursor-default',
|
|
1684
|
-
cell.today && !cell.hidden && todayIndicator === 'cell' && 'ring-2 ring-inset ring-ring/40',
|
|
1684
|
+
cell.today && !cell.hidden && (todayIndicator === 'cell' || todayIndicator === 'both') && 'ring-2 ring-inset ring-ring/40',
|
|
1685
1685
|
dayClassFor(cell),
|
|
1686
1686
|
dragAndDrop && dragState.active && canDropOnCell(cell) && !isDropTarget(cell) && 'bg-primary/5',
|
|
1687
1687
|
dragAndDrop && isDropTarget(cell) && 'bg-primary/10 ring-2 ring-inset ring-ring/60',
|
|
@@ -1698,10 +1698,9 @@ function onDayClick(cell, event) {
|
|
|
1698
1698
|
<template v-if="!cell.hidden">
|
|
1699
1699
|
<div class="flex items-center justify-between gap-2">
|
|
1700
1700
|
<span
|
|
1701
|
-
class="text-base font-bold leading-none tabular-nums"
|
|
1701
|
+
class="inline-flex size-7 shrink-0 items-center justify-center rounded-full text-base font-bold leading-none tabular-nums"
|
|
1702
1702
|
:class="[
|
|
1703
|
-
todayIndicator === 'circle'
|
|
1704
|
-
cell.today && todayIndicator === 'circle' && resolveDayClass(todayDateClass, cell),
|
|
1703
|
+
cell.today && (todayIndicator === 'circle' || todayIndicator === 'both') && resolveDayClass(todayDateClass, cell),
|
|
1705
1704
|
]"
|
|
1706
1705
|
>{{ cell.day }}</span>
|
|
1707
1706
|
<span v-if="showDayNames" class="text-base lowercase leading-none tabular-nums">{{ cell.weekday }}</span>
|
|
@@ -2644,11 +2644,11 @@ export interface DomMonthCalendarProps {
|
|
|
2644
2644
|
*/
|
|
2645
2645
|
showDayNames?: boolean;
|
|
2646
2646
|
/**
|
|
2647
|
-
* Highlight today with a cell outline, a coloured circle behind the date, or no visual indicator.
|
|
2647
|
+
* Highlight today with a cell outline, a coloured circle behind the date, both, or no visual indicator.
|
|
2648
2648
|
*/
|
|
2649
|
-
todayIndicator?: "cell" | "circle" | "none";
|
|
2649
|
+
todayIndicator?: "cell" | "circle" | "both" | "none";
|
|
2650
2650
|
/**
|
|
2651
|
-
* Classes for today’s date circle. Replaces the default primary background and foreground colours when todayIndicator is circle.
|
|
2651
|
+
* Classes for today’s date circle. Replaces the default primary background and foreground colours when todayIndicator is circle or both.
|
|
2652
2652
|
*/
|
|
2653
2653
|
todayDateClass?: string | Array<unknown> | Record<string, unknown> | ((...args: Array<unknown>) => unknown);
|
|
2654
2654
|
/**
|
package/src/tailwind.css
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
--dom-month-calendar-row-rail-direction: row;
|
|
15
15
|
--dom-month-calendar-row-rail-justify: space-between;
|
|
16
16
|
--dom-month-calendar-row-rail-min-height: 2.5rem;
|
|
17
|
+
--dom-month-calendar-row-rail-padding: 0.5rem;
|
|
18
|
+
--dom-month-calendar-week-number-order: 0;
|
|
17
19
|
--dom-month-calendar-week-columns: minmax(0, 1fr);
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -33,6 +35,8 @@
|
|
|
33
35
|
--dom-month-calendar-row-rail-direction: column;
|
|
34
36
|
--dom-month-calendar-row-rail-justify: flex-start;
|
|
35
37
|
--dom-month-calendar-row-rail-min-height: 11rem;
|
|
38
|
+
--dom-month-calendar-row-rail-padding: 0.75rem;
|
|
39
|
+
--dom-month-calendar-week-number-order: -1;
|
|
36
40
|
--dom-month-calendar-week-columns: var(--dom-month-calendar-wide-week-columns);
|
|
37
41
|
--dom-month-calendar-wide-border-left-width: 1px;
|
|
38
42
|
--dom-month-calendar-wide-border-top-width: 0;
|
|
@@ -109,7 +113,7 @@
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
.skin-control {
|
|
112
|
-
background-color: var(--skin-control-bg, var(--
|
|
116
|
+
background-color: var(--skin-control-bg, var(--canvas));
|
|
113
117
|
background-image: var(--skin-control-bg-image, none);
|
|
114
118
|
border-color: var(--skin-control-border, var(--border));
|
|
115
119
|
border-style: var(--skin-control-border-style, solid);
|
package/src/theme.css
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
--shadow-lg: 0 8px 20px -6px rgb(15 23 42 / 0.16), 0 2px 6px -2px rgb(15 23 42 / 0.08);
|
|
12
12
|
--shadow-xl: 0 18px 44px -18px rgb(15 23 42 / 0.24), 0 8px 18px -12px rgb(15 23 42 / 0.16);
|
|
13
13
|
--shadow-2xl: 0 24px 60px -18px rgb(15 23 42 / 0.24), 0 8px 20px -10px rgb(15 23 42 / 0.14);
|
|
14
|
+
--dom-command-palette-background: color-mix(in oklch, var(--canvas) 68%, transparent);
|
|
15
|
+
--dom-command-palette-shadow: 0 40px 100px -24px rgb(15 23 42 / 0.3), 0 16px 40px -20px rgb(15 23 42 / 0.2);
|
|
14
16
|
--chart-1: oklch(0.55 0.19 257);
|
|
15
17
|
--chart-2: oklch(0.58 0.13 175);
|
|
16
18
|
--chart-3: oklch(0.65 0.16 65);
|
|
@@ -45,6 +47,8 @@
|
|
|
45
47
|
--shadow-lg: 0 1px 0 0 rgb(255 255 255 / 0.05) inset, 0 16px 34px -18px rgb(0 0 0 / 0.58), 0 5px 12px -8px rgb(0 0 0 / 0.5);
|
|
46
48
|
--shadow-xl: 0 1px 0 0 rgb(255 255 255 / 0.05) inset, 0 24px 52px -24px rgb(0 0 0 / 0.68), 0 10px 22px -14px rgb(0 0 0 / 0.56);
|
|
47
49
|
--shadow-2xl: 0 1px 0 0 rgb(255 255 255 / 0.06) inset, 0 34px 76px -30px rgb(0 0 0 / 0.72), 0 16px 34px -20px rgb(0 0 0 / 0.58);
|
|
50
|
+
--dom-command-palette-background: color-mix(in oklch, var(--canvas) 68%, transparent);
|
|
51
|
+
--dom-command-palette-shadow: 0 40px 100px -24px rgb(0 0 0 / 0.78), 0 16px 40px -20px rgb(0 0 0 / 0.62);
|
|
48
52
|
--chart-1: oklch(0.72 0.15 257);
|
|
49
53
|
--chart-2: oklch(0.75 0.13 175);
|
|
50
54
|
--chart-3: oklch(0.8 0.15 75);
|