@frockbot/plugin-routines 0.0.0 → 0.1.1

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.
@@ -0,0 +1,216 @@
1
+ <script setup lang="ts">
2
+ // The completion inbox, in the shell header.
3
+ //
4
+ // A Routine firing cannot speak to the user: it has no `send_to_user`, and its
5
+ // Turn is filtered out of the visible transcript. So the only place a
6
+ // completion becomes visible is here — a count of what has not been read, and a
7
+ // drawer that reads it. Acknowledging is a command, never a side effect of
8
+ // opening the drawer, so a glance does not clear the badge.
9
+ import { UiButton, UiIcon } from "@frockbot/client-ui";
10
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
11
+ import { computed, inject, ref, watch } from "vue";
12
+ import { routinesStateKey } from "./state.js";
13
+
14
+ const providedWeb = inject(frockBotWebDataKey);
15
+ const providedState = inject(routinesStateKey);
16
+ if (!providedWeb || !providedState) {
17
+ throw new Error("Routines client services were not provided");
18
+ }
19
+ const web = providedWeb;
20
+ const routines = providedState;
21
+
22
+ const open = ref(false);
23
+ const botId = computed(() => web.value.activeBotId);
24
+ const count = computed(() => routines.value.unacknowledged);
25
+ const badge = computed(() => (count.value > 99 ? "99+" : String(count.value)));
26
+
27
+ watch(
28
+ botId,
29
+ (id) => {
30
+ open.value = false;
31
+ if (id) void routines.value.loadInbox(id);
32
+ },
33
+ { immediate: true },
34
+ );
35
+
36
+ function toggle(): void {
37
+ open.value = !open.value;
38
+ if (open.value && botId.value) void routines.value.loadInbox(botId.value);
39
+ }
40
+
41
+ function acknowledge(entryIds: string[]): void {
42
+ if (!botId.value) return;
43
+ void routines.value.acknowledgeInbox(botId.value, entryIds);
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <div v-if="botId" class="routine-inbox">
49
+ <button
50
+ type="button"
51
+ class="routine-inbox__trigger"
52
+ :aria-expanded="open"
53
+ :title="`Routine completions${count > 0 ? ` (${badge} unread)` : ''}`"
54
+ @click="toggle"
55
+ >
56
+ <UiIcon name="history" />
57
+ <span v-if="count > 0" class="routine-inbox__badge">{{ badge }}</span>
58
+ </button>
59
+
60
+ <div v-if="open" class="routine-inbox__drawer">
61
+ <header class="routine-inbox__header">
62
+ <h2>Routine completions</h2>
63
+ <UiButton
64
+ v-if="count > 0"
65
+ variant="ghost"
66
+ :disabled="routines.busy"
67
+ @click="acknowledge([])"
68
+ >Mark all read</UiButton
69
+ >
70
+ </header>
71
+ <p v-if="routines.inbox.length === 0" class="routine-inbox__empty">
72
+ Nothing has been left here. A Routine that finishes lands its message in
73
+ this drawer rather than in the conversation.
74
+ </p>
75
+ <ul v-else class="routine-inbox__list">
76
+ <li
77
+ v-for="entry in routines.inbox"
78
+ :key="entry.entryId"
79
+ :class="{ 'routine-inbox__item--read': entry.acknowledged }"
80
+ class="routine-inbox__item"
81
+ >
82
+ <p class="routine-inbox__attribution">{{ entry.attribution }}</p>
83
+ <p class="routine-inbox__text">{{ entry.text }}</p>
84
+ <footer class="routine-inbox__meta">
85
+ <span>{{ entry.createdAt }}</span>
86
+ <UiButton
87
+ v-if="!entry.acknowledged"
88
+ variant="ghost"
89
+ :disabled="routines.busy"
90
+ @click="acknowledge([entry.entryId])"
91
+ >Mark read</UiButton
92
+ >
93
+ </footer>
94
+ </li>
95
+ </ul>
96
+ </div>
97
+ </div>
98
+ </template>
99
+
100
+ <style scoped>
101
+ .routine-inbox {
102
+ position: relative;
103
+ }
104
+
105
+ .routine-inbox__trigger {
106
+ position: relative;
107
+ display: grid;
108
+ width: var(--frock-avatar-sm);
109
+ height: var(--frock-avatar-sm);
110
+ place-items: center;
111
+ border: 0;
112
+ border-radius: 8px;
113
+ color: var(--frock-text-muted);
114
+ background: transparent;
115
+ cursor: pointer;
116
+ }
117
+
118
+ .routine-inbox__trigger:hover {
119
+ color: var(--frock-text);
120
+ background: var(--frock-surface);
121
+ }
122
+
123
+ .routine-inbox__badge {
124
+ position: absolute;
125
+ top: -2px;
126
+ right: -2px;
127
+ min-width: 16px;
128
+ padding: 0 4px;
129
+ border-radius: 8px;
130
+ color: var(--frock-on-accent);
131
+ background: var(--frock-action-primary);
132
+ font-size: var(--frock-text-xs);
133
+ line-height: 16px;
134
+ text-align: center;
135
+ }
136
+
137
+ .routine-inbox__drawer {
138
+ position: absolute;
139
+ z-index: 20;
140
+ top: calc(100% + 8px);
141
+ right: 0;
142
+ display: flex;
143
+ width: 340px;
144
+ max-height: 60vh;
145
+ flex-direction: column;
146
+ gap: 10px;
147
+ overflow-y: auto;
148
+ padding: 12px;
149
+ border-radius: 12px;
150
+ background: var(--frock-surface);
151
+ box-shadow: inset 0 0 0 1px var(--frock-border);
152
+ }
153
+
154
+ .routine-inbox__header {
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: space-between;
158
+ gap: 8px;
159
+ }
160
+
161
+ .routine-inbox__header h2 {
162
+ margin: 0;
163
+ color: var(--frock-text);
164
+ font-size: var(--frock-text-sm);
165
+ }
166
+
167
+ .routine-inbox__empty {
168
+ margin: 0;
169
+ color: var(--frock-text-muted);
170
+ font-size: var(--frock-text-sm);
171
+ }
172
+
173
+ .routine-inbox__list {
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 10px;
177
+ margin: 0;
178
+ padding: 0;
179
+ list-style: none;
180
+ }
181
+
182
+ .routine-inbox__item {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 4px;
186
+ padding: 8px;
187
+ border-radius: 8px;
188
+ background: var(--frock-surface-raised);
189
+ }
190
+
191
+ .routine-inbox__item--read {
192
+ opacity: 0.6;
193
+ }
194
+
195
+ .routine-inbox__attribution {
196
+ margin: 0;
197
+ color: var(--frock-text-subtle);
198
+ font-size: var(--frock-text-xs);
199
+ }
200
+
201
+ .routine-inbox__text {
202
+ margin: 0;
203
+ color: var(--frock-text);
204
+ font-size: var(--frock-text-sm);
205
+ white-space: pre-wrap;
206
+ }
207
+
208
+ .routine-inbox__meta {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: space-between;
212
+ gap: 8px;
213
+ color: var(--frock-text-muted);
214
+ font-size: var(--frock-text-xs);
215
+ }
216
+ </style>