@love-moon/app-sdk 0.4.2 → 0.5.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.
@@ -99,13 +99,23 @@
99
99
 
100
100
  /* ----- Message list ------------------------------------------------------- */
101
101
 
102
+ /* Positioning context for the scroll-to-bottom button, the question-anchor
103
+ * rail, and the action-menu overlay — all of which are absolutely positioned
104
+ * relative to the scroll area (not the page). This wrapper is the `1fr` grid
105
+ * row; the inner list scrolls inside it. */
106
+ .conductor-message-list-viewport {
107
+ position: relative;
108
+ min-height: 0; /* lets it shrink inside the grid row */
109
+ overflow: hidden; /* contain the absolutely-positioned affordances */
110
+ }
111
+
102
112
  .conductor-message-list {
113
+ height: 100%;
103
114
  overflow-y: auto;
104
115
  padding: var(--conductor-spacing);
105
116
  display: flex;
106
117
  flex-direction: column;
107
118
  gap: 8px;
108
- min-height: 0; /* lets it shrink inside the grid row */
109
119
  }
110
120
 
111
121
  .conductor-load-earlier {
@@ -141,8 +151,27 @@
141
151
  justify-content: flex-start;
142
152
  }
143
153
 
144
- .conductor-bubble {
154
+ /* Wrapper that stacks the bubble + its timestamp + attachments. This is the
155
+ * flex child that gets aligned left/right; `.conductor-bubble` inside it holds
156
+ * only the message text. */
157
+ .conductor-bubble-group {
158
+ position: relative;
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 4px;
145
162
  max-width: 75%;
163
+ min-width: 0;
164
+ }
165
+
166
+ .conductor-message--user .conductor-bubble-group {
167
+ align-items: flex-end;
168
+ }
169
+
170
+ .conductor-message--assistant .conductor-bubble-group {
171
+ align-items: flex-start;
172
+ }
173
+
174
+ .conductor-bubble {
146
175
  padding: 10px 14px;
147
176
  border-radius: var(--conductor-radius);
148
177
  white-space: pre-wrap;
@@ -164,6 +193,153 @@
164
193
  opacity: 0.6;
165
194
  }
166
195
 
196
+ /* Hover/tap-revealed timestamp, absolutely positioned so it never reserves
197
+ * vertical space in the list. */
198
+ .conductor-bubble__time {
199
+ position: absolute;
200
+ top: -13px;
201
+ font-size: 10px;
202
+ line-height: 1;
203
+ color: var(--conductor-text-muted);
204
+ white-space: nowrap;
205
+ opacity: 0;
206
+ transition: opacity 120ms ease;
207
+ pointer-events: none;
208
+ }
209
+
210
+ .conductor-message--user .conductor-bubble__time {
211
+ right: 4px;
212
+ }
213
+
214
+ .conductor-message--assistant .conductor-bubble__time {
215
+ left: 4px;
216
+ }
217
+
218
+ .conductor-bubble-group:hover .conductor-bubble__time,
219
+ .conductor-bubble-group--timestamp-visible .conductor-bubble__time {
220
+ opacity: 1;
221
+ }
222
+
223
+ /* "via {app}" origin chip rendered inline at the start of the bubble. */
224
+ .conductor-bubble__origin {
225
+ display: inline-block;
226
+ margin-right: 6px;
227
+ padding: 1px 6px;
228
+ border-radius: 999px;
229
+ font-size: 11px;
230
+ background: var(--conductor-border);
231
+ color: var(--conductor-text-muted);
232
+ vertical-align: middle;
233
+ }
234
+
235
+ /* ----- Attachments -------------------------------------------------------- */
236
+
237
+ .conductor-bubble__attachments {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 8px;
241
+ width: 100%;
242
+ }
243
+
244
+ .conductor-attachment {
245
+ overflow: hidden;
246
+ border: 1px solid var(--conductor-border);
247
+ border-radius: var(--conductor-radius);
248
+ background: var(--conductor-bubble-assistant);
249
+ }
250
+
251
+ .conductor-attachment__media-link {
252
+ display: block;
253
+ }
254
+
255
+ .conductor-attachment__image {
256
+ display: block;
257
+ width: 100%;
258
+ max-height: 28rem;
259
+ object-fit: cover;
260
+ }
261
+
262
+ .conductor-attachment__video {
263
+ display: block;
264
+ width: 100%;
265
+ max-height: 28rem;
266
+ background: #000;
267
+ }
268
+
269
+ .conductor-attachment__audio {
270
+ display: block;
271
+ width: 100%;
272
+ padding: 8px;
273
+ }
274
+
275
+ .conductor-attachment__file {
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: space-between;
279
+ gap: 12px;
280
+ padding: 10px 12px;
281
+ font-size: 13px;
282
+ color: var(--conductor-text);
283
+ text-decoration: none;
284
+ }
285
+
286
+ .conductor-attachment__meta {
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: space-between;
290
+ gap: 12px;
291
+ padding: 6px 12px;
292
+ font-size: 11px;
293
+ color: var(--conductor-text-muted);
294
+ }
295
+
296
+ .conductor-attachment__name {
297
+ overflow: hidden;
298
+ text-overflow: ellipsis;
299
+ white-space: nowrap;
300
+ }
301
+
302
+ .conductor-attachment__size {
303
+ flex-shrink: 0;
304
+ color: var(--conductor-text-muted);
305
+ }
306
+
307
+ /* ----- Empty state -------------------------------------------------------- */
308
+
309
+ .conductor-empty {
310
+ display: flex;
311
+ flex-direction: column;
312
+ align-items: center;
313
+ justify-content: center;
314
+ flex: 1;
315
+ gap: 6px;
316
+ padding: 32px 16px;
317
+ text-align: center;
318
+ color: var(--conductor-text-muted);
319
+ }
320
+
321
+ .conductor-empty__icon {
322
+ width: 48px;
323
+ height: 48px;
324
+ opacity: 0.35;
325
+ margin-bottom: 4px;
326
+ }
327
+
328
+ .conductor-empty__title {
329
+ font-size: 16px;
330
+ font-weight: 600;
331
+ color: var(--conductor-text);
332
+ }
333
+
334
+ .conductor-empty__body {
335
+ max-width: 30rem;
336
+ font-size: 13px;
337
+ }
338
+
339
+ .conductor-empty__restart {
340
+ margin-top: 12px;
341
+ }
342
+
167
343
  /* ----- Message input ------------------------------------------------------ */
168
344
 
169
345
  .conductor-message-input {
@@ -244,21 +420,191 @@
244
420
  filter: brightness(0.92);
245
421
  }
246
422
 
423
+ /* ----- Scroll-to-bottom button -------------------------------------------- */
424
+
425
+ .conductor-scroll-to-bottom {
426
+ position: absolute;
427
+ right: 12px;
428
+ bottom: 12px;
429
+ z-index: 10;
430
+ display: flex;
431
+ align-items: center;
432
+ justify-content: center;
433
+ width: 36px;
434
+ height: 36px;
435
+ padding: 0;
436
+ border-radius: 50%;
437
+ border: 1px solid var(--conductor-border);
438
+ background: var(--conductor-bubble-assistant);
439
+ color: var(--conductor-text);
440
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
441
+ cursor: pointer;
442
+ transition: background-color 120ms ease;
443
+ }
444
+
445
+ .conductor-scroll-to-bottom:hover {
446
+ background: rgba(0, 0, 0, 0.04);
447
+ }
448
+
449
+ .conductor-scroll-to-bottom svg {
450
+ width: 16px;
451
+ height: 16px;
452
+ }
453
+
454
+ /* ----- Question anchor navigation ----------------------------------------- */
455
+
456
+ .conductor-question-nav {
457
+ position: absolute;
458
+ right: 8px;
459
+ top: 50%;
460
+ transform: translateY(-50%);
461
+ z-index: 20;
462
+ display: flex;
463
+ flex-direction: column;
464
+ align-items: center;
465
+ max-height: 80%;
466
+ overflow-y: auto;
467
+ padding: 4px 0;
468
+ transition: opacity 200ms ease;
469
+ scrollbar-width: none; /* Firefox: hide the rail's own scrollbar */
470
+ }
471
+
472
+ .conductor-question-nav::-webkit-scrollbar {
473
+ width: 0;
474
+ height: 0;
475
+ }
476
+
477
+ .conductor-question-nav--visible {
478
+ opacity: 1;
479
+ pointer-events: auto;
480
+ }
481
+
482
+ .conductor-question-nav--hidden {
483
+ opacity: 0;
484
+ pointer-events: none;
485
+ }
486
+
487
+ .conductor-question-nav__group {
488
+ display: flex;
489
+ flex-direction: column;
490
+ align-items: center;
491
+ }
492
+
493
+ .conductor-question-nav__sep {
494
+ width: 1px;
495
+ height: 12px;
496
+ background: rgba(0, 0, 0, 0.2);
497
+ }
498
+
499
+ .conductor-question-nav__btn {
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: center;
503
+ width: 24px;
504
+ height: 24px;
505
+ padding: 0;
506
+ border: 0;
507
+ background: transparent;
508
+ cursor: pointer;
509
+ }
510
+
511
+ .conductor-question-nav__dot {
512
+ display: block;
513
+ width: 6px;
514
+ height: 6px;
515
+ border-radius: 50%;
516
+ background: rgba(0, 0, 0, 0.35);
517
+ transition: all 150ms ease;
518
+ }
519
+
520
+ .conductor-question-nav__btn:hover .conductor-question-nav__dot {
521
+ background: rgba(0, 0, 0, 0.6);
522
+ }
523
+
524
+ .conductor-question-nav__dot--active {
525
+ width: 12px;
526
+ height: 12px;
527
+ background: var(--accent);
528
+ }
529
+
530
+ /* ----- Bubble action menu (double-click / double-tap) --------------------- */
531
+
532
+ .conductor-bubble-menu-overlay {
533
+ position: absolute;
534
+ inset: 0;
535
+ z-index: 50;
536
+ }
537
+
538
+ .conductor-bubble-menu-backdrop {
539
+ position: absolute;
540
+ inset: 0;
541
+ background: rgba(0, 0, 0, 0.2);
542
+ }
543
+
544
+ .conductor-bubble-menu-sheet {
545
+ position: absolute;
546
+ left: 0;
547
+ right: 0;
548
+ bottom: 0;
549
+ padding: 12px;
550
+ border-top: 1px solid var(--conductor-border);
551
+ border-top-left-radius: 16px;
552
+ border-top-right-radius: 16px;
553
+ background: var(--conductor-bubble-assistant);
554
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.18);
555
+ }
556
+
557
+ .conductor-bubble-menu-handle {
558
+ width: 48px;
559
+ height: 5px;
560
+ margin: 0 auto 12px;
561
+ border-radius: 999px;
562
+ background: var(--conductor-border);
563
+ }
564
+
565
+ .conductor-bubble-menu-actions {
566
+ display: flex;
567
+ align-items: center;
568
+ justify-content: center;
569
+ gap: 8px;
570
+ }
571
+
572
+ .conductor-bubble-menu-item {
573
+ flex: 0 0 auto;
574
+ padding: 8px 16px;
575
+ border-radius: var(--conductor-radius);
576
+ border: 1px solid var(--conductor-border);
577
+ background: var(--conductor-paper);
578
+ color: var(--conductor-text);
579
+ font: inherit;
580
+ cursor: pointer;
581
+ transition: background-color 120ms ease;
582
+ }
583
+
584
+ .conductor-bubble-menu-item:hover {
585
+ background: rgba(0, 0, 0, 0.04);
586
+ }
587
+
588
+ .conductor-bubble-menu-item:disabled {
589
+ opacity: 0.4;
590
+ cursor: not-allowed;
591
+ }
592
+
247
593
  /* ----- Mobile layout ------------------------------------------------------ */
248
594
 
249
595
  @media (max-width: 600px) {
250
596
  .conductor-chat-view {
251
597
  font-size: 15px;
252
598
  }
253
- .conductor-bubble {
599
+ .conductor-bubble-group {
254
600
  max-width: 85%;
255
601
  }
256
602
  }
257
603
 
258
- .conductor-chat-view[data-layout='mobile'] .conductor-bubble {
604
+ .conductor-chat-view[data-layout='mobile'] .conductor-bubble-group {
259
605
  max-width: 85%;
260
606
  }
261
607
 
262
- .conductor-chat-view[data-layout='desktop'] .conductor-bubble {
608
+ .conductor-chat-view[data-layout='desktop'] .conductor-bubble-group {
263
609
  max-width: 60%;
264
610
  }
@@ -322,6 +322,11 @@ declare class TasksApi {
322
322
  targetReplyTo: string;
323
323
  signal?: AbortSignal;
324
324
  }): Promise<void>;
325
+ /** Restart the task's AI session (e.g. `restartMode: 'refresh_session'`). */
326
+ restart(taskId: string, opts?: {
327
+ restartMode?: string;
328
+ signal?: AbortSignal;
329
+ }): Promise<void>;
325
330
  /**
326
331
  * Subscribe to a task's event stream. Yields ChatEvents until the caller
327
332
  * breaks out of the `for await` loop, calls `signal.abort()`, or the
@@ -564,6 +564,27 @@ var TasksRestApi = class {
564
564
  signal: opts.signal
565
565
  });
566
566
  }
567
+ /**
568
+ * Restart the task's AI session. `restartMode` mirrors Conductor's REST
569
+ * contract (e.g. `'refresh_session'`); when omitted the backend chooses the
570
+ * default. The effect is observed via the event stream (runtime_status / new
571
+ * messages), so this resolves to void.
572
+ */
573
+ async restart(taskId, opts) {
574
+ if (!taskId) {
575
+ throw new ConductorAppError({
576
+ code: "invalid_input",
577
+ message: "tasks.restart requires a taskId"
578
+ });
579
+ }
580
+ const restartMode = opts?.restartMode?.trim();
581
+ await this.fetcher.request({
582
+ method: "POST",
583
+ path: `/api/tasks/${encodeURIComponent(taskId)}/restart`,
584
+ body: restartMode ? { restart_mode: restartMode } : {},
585
+ signal: opts?.signal
586
+ });
587
+ }
567
588
  };
568
589
  function buildOutboundMetadata(callerMetadata) {
569
590
  const base = callerMetadata && typeof callerMetadata === "object" ? { ...callerMetadata } : {};
@@ -1247,6 +1268,11 @@ var TasksApi = class {
1247
1268
  this.assertOpen();
1248
1269
  return this.rest.interrupt(taskId, opts);
1249
1270
  }
1271
+ /** Restart the task's AI session (e.g. `restartMode: 'refresh_session'`). */
1272
+ restart(taskId, opts) {
1273
+ this.assertOpen();
1274
+ return this.rest.restart(taskId, opts);
1275
+ }
1250
1276
  /**
1251
1277
  * Subscribe to a task's event stream. Yields ChatEvents until the caller
1252
1278
  * breaks out of the `for await` loop, calls `signal.abort()`, or the
@@ -114,6 +114,15 @@ export async function POST(req: NextRequest, ctx: RouteContext) {
114
114
  return NextResponse.json({ ok: true });
115
115
  }
116
116
 
117
+ if (op === 'restart') {
118
+ // Only reached when the widget's adapter is created with
119
+ // `enableRestart: true` (see app/page.tsx). Forward to Conductor.
120
+ const restartMode =
121
+ typeof body?.restart_mode === 'string' ? body.restart_mode : 'refresh_session';
122
+ await client.tasks.restart(taskId, { restartMode });
123
+ return NextResponse.json({ ok: true });
124
+ }
125
+
117
126
  return notFound();
118
127
  } catch (err) {
119
128
  return errorResponse(err);
@@ -26,6 +26,9 @@ const adapter = createRestAdapter({
26
26
  baseUrl: '/api/conductor',
27
27
  // No authToken — we trust the browser session and let the BFF authenticate
28
28
  // upstream via its server-held Conductor token.
29
+ // Opt into restart: the catch-all route forwards POST /tasks/:id/restart to
30
+ // client.tasks.restart(). Omit this to hide all restart UI.
31
+ enableRestart: true,
29
32
  });
30
33
 
31
34
  export default function Page() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/app-sdk",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Conductor App SDK: third-party backend SDK + React chat widget for embedding Conductor AI tools",
5
5
  "repository": {
6
6
  "type": "git",