@jskit-ai/assistant-core 0.2.11 → 0.2.13
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/examples/conversation/package.json +1 -1
- package/fixtures/responsive-assistant/ConversationFixture.vue +1 -0
- package/fixtures/responsive-assistant/DeliveryFixture.vue +5 -1
- package/fixtures/responsive-assistant/main.js +2 -2
- package/package.json +1 -1
- package/src/client/conversation/AssistantGoalControl.vue +1 -1
- package/src/client/conversation/AssistantTranscript.css +4 -4
- package/test/assistantScroll.browser.test.js +4 -0
- package/test/transcriptTheme.browser.test.js +87 -0
|
@@ -179,6 +179,7 @@ const adapter = reactive({
|
|
|
179
179
|
<button v-if="capabilitiesEnabled" @click="finishUploads">Finish uploads</button>
|
|
180
180
|
<button @click="goalEnabled = !goalEnabled">Toggle goals</button>
|
|
181
181
|
<button v-if="goal" @click="goal = { ...goal, status: 'blocked', reason: 'An explanation of the blocker. '.repeat(200) }">Block goal with long explanation</button>
|
|
182
|
+
<button v-if="goal" @click="goal = { ...goal, status: 'complete' }">Complete goal</button>
|
|
182
183
|
<template v-if="previewFixture">
|
|
183
184
|
<button @click="temporaryWords = 'Exact temporary words'; temporaryPresent = true; temporaryEditing = false">Add temporary message</button>
|
|
184
185
|
<button @click="newerWords = 'Keep newer live words'; newerPresent = true; newerEditing = false">Add newer temporary message</button>
|
|
@@ -5,6 +5,7 @@ import { createAssistantMessageDelivery } from "../../src/client/conversation/me
|
|
|
5
5
|
|
|
6
6
|
const recoveryFixture = new URLSearchParams(location.search).has("recovery");
|
|
7
7
|
const acceptedFixture = new URLSearchParams(location.search).has("accepted-history");
|
|
8
|
+
const taskFixture = new URLSearchParams(location.search).has("task");
|
|
8
9
|
const draft = ref("");
|
|
9
10
|
const turns = ref([]);
|
|
10
11
|
const requests = ref([]);
|
|
@@ -63,7 +64,10 @@ if (acceptedFixture) {
|
|
|
63
64
|
}
|
|
64
65
|
const adapter = reactive({
|
|
65
66
|
delivery, attachments,
|
|
66
|
-
conversation: {
|
|
67
|
+
conversation: {
|
|
68
|
+
cancellableDelivery: recoveryFixture, turns, visible: true, scrollKey: "delivery", welcomeMessage: "Start a conversation.",
|
|
69
|
+
variant: taskFixture ? "task" : "main"
|
|
70
|
+
},
|
|
67
71
|
composer: { draft, canSend: true, submitOnEnter: true, queueWhileSending },
|
|
68
72
|
actions: {
|
|
69
73
|
setDraft(value) { draft.value = value; },
|
|
@@ -9,6 +9,7 @@ import ConversationFixture from "./ConversationFixture.vue";
|
|
|
9
9
|
import ProgressFixture from "./ProgressFixture.vue";
|
|
10
10
|
import DeliveryFixture from "./DeliveryFixture.vue";
|
|
11
11
|
|
|
12
|
+
const query = new URLSearchParams(location.search);
|
|
12
13
|
const vuetify = createVuetify({
|
|
13
14
|
components,
|
|
14
15
|
directives,
|
|
@@ -18,11 +19,10 @@ const vuetify = createVuetify({
|
|
|
18
19
|
sets: { mdi }
|
|
19
20
|
},
|
|
20
21
|
theme: {
|
|
21
|
-
defaultTheme: "light"
|
|
22
|
+
defaultTheme: query.has("dark") ? "dark" : "light"
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
25
|
|
|
25
|
-
const query = new URLSearchParams(location.search);
|
|
26
26
|
createApp(query.has("delivery") ? DeliveryFixture : query.has("progress") ? ProgressFixture : query.has("conversation") ? ConversationFixture : App)
|
|
27
27
|
.use(vuetify)
|
|
28
28
|
.mount("#app");
|
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@ async function setGoal() {
|
|
|
71
71
|
<v-btn v-bind="menuProps" class="assistant-goal" size="small" variant="text" :aria-label="label" :title="[label, elapsedText, goal?.objective].filter(Boolean).join('\n')">
|
|
72
72
|
<span v-if="goal" class="assistant-goal__light" :class="{ 'assistant-goal__light--running': running, 'assistant-goal__light--working': working, 'assistant-goal__light--paused': goal.status === 'paused' }" aria-hidden="true" />
|
|
73
73
|
<span class="assistant-goal__label">{{ state.loading ? t('jskit.assistant.goal.loading') : state.available === false ? t('jskit.assistant.goal.unavailable') : starting ? t('jskit.assistant.goal.starting') : goal ? t('jskit.assistant.goal.goal') : t('jskit.assistant.goal.set') }}</span>
|
|
74
|
-
<span v-if="elapsed" class="assistant-goal__elapsed" aria-hidden="true">{{ elapsed }}</span>
|
|
74
|
+
<span v-if="elapsed && goal.status !== 'complete'" class="assistant-goal__elapsed" aria-hidden="true">{{ elapsed }}</span>
|
|
75
75
|
</v-btn>
|
|
76
76
|
</template>
|
|
77
77
|
<v-card class="assistant-goal__details pa-3" max-width="360">
|
|
@@ -248,8 +248,8 @@
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
.assistant-transcript--task .assistant-transcript__message--user {
|
|
251
|
-
background:
|
|
252
|
-
color:
|
|
251
|
+
background: rgba(126, 87, 194, 0.16);
|
|
252
|
+
color: rgb(var(--v-theme-on-surface));
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
.assistant-transcript__thinking {
|
|
@@ -325,13 +325,13 @@
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
.assistant-transcript__message-header time {
|
|
328
|
-
color:
|
|
328
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
329
329
|
font-weight: 650;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
.assistant-transcript__message-footer {
|
|
333
333
|
align-items: center;
|
|
334
|
-
color:
|
|
334
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
335
335
|
display: flex;
|
|
336
336
|
font-size: 0.88rem;
|
|
337
337
|
font-weight: 500;
|
|
@@ -1025,6 +1025,10 @@ test("optional goals show running and paused lights, count only active time, and
|
|
|
1025
1025
|
await expect(page.getByRole("button", { name: "Goal paused", exact: true })).toBeVisible();
|
|
1026
1026
|
await page.getByRole("button", { name: "Resume goal", exact: true }).click();
|
|
1027
1027
|
await expect(page.getByRole("button", { name: "Pause goal", exact: true })).toBeVisible();
|
|
1028
|
+
// A finished goal keeps its running time in its details, not as a number beside it.
|
|
1029
|
+
await page.getByRole("button", { name: "Complete goal", exact: true }).evaluate(element => element.click());
|
|
1030
|
+
await expect(page.getByRole("button", { name: "Goal complete", exact: true })).toBeVisible();
|
|
1031
|
+
await expect(elapsed).toHaveCount(0);
|
|
1028
1032
|
await page.getByRole("button", { name: "Toggle goals" }).evaluate(element => element.click());
|
|
1029
1033
|
await expect(page.locator(".assistant-goal")).toHaveCount(0);
|
|
1030
1034
|
await expect(page.getByRole("button", { name: "Pause goal", exact: true })).toBeHidden();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { chromium, expect } from "@playwright/test";
|
|
5
|
+
import { createChromiumLaunchOptions, startViteFixture, stopProcess } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
6
|
+
|
|
7
|
+
// An element's text colour, the theme values it should come from, and its WCAG contrast over what is painted under
|
|
8
|
+
// it: each translucent background is laid over the next one out, down to the first opaque one (white when none is).
|
|
9
|
+
// Runs in the page.
|
|
10
|
+
function readText(element) {
|
|
11
|
+
const channels = (value) => {
|
|
12
|
+
const [red, green, blue, alpha = 1] = value.match(/[\d.]+/gu).map(Number);
|
|
13
|
+
return { red, green, blue, alpha };
|
|
14
|
+
};
|
|
15
|
+
const over = (top, under) => ({
|
|
16
|
+
red: top.red * top.alpha + under.red * (1 - top.alpha),
|
|
17
|
+
green: top.green * top.alpha + under.green * (1 - top.alpha),
|
|
18
|
+
blue: top.blue * top.alpha + under.blue * (1 - top.alpha),
|
|
19
|
+
alpha: 1
|
|
20
|
+
});
|
|
21
|
+
const luminance = ({ red, green, blue }) => {
|
|
22
|
+
const [r, g, b] = [red, green, blue].map((channel) => {
|
|
23
|
+
const value = channel / 255;
|
|
24
|
+
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
25
|
+
});
|
|
26
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
27
|
+
};
|
|
28
|
+
const layers = [];
|
|
29
|
+
for (let node = element; node; node = node.parentElement) {
|
|
30
|
+
const background = channels(getComputedStyle(node).backgroundColor);
|
|
31
|
+
if (background.alpha > 0) layers.push(background);
|
|
32
|
+
if (background.alpha === 1) break;
|
|
33
|
+
}
|
|
34
|
+
const backdrop = layers.reverse().reduce((under, layer) => over(layer, under), { red: 255, green: 255, blue: 255, alpha: 1 });
|
|
35
|
+
const style = getComputedStyle(element);
|
|
36
|
+
const color = channels(style.color);
|
|
37
|
+
const [lighter, darker] = [luminance(over(color, backdrop)), luminance(backdrop)].sort((a, b) => b - a);
|
|
38
|
+
return {
|
|
39
|
+
color: [color.red, color.green, color.blue, color.alpha],
|
|
40
|
+
onSurface: style.getPropertyValue("--v-theme-on-surface").split(",").map(Number),
|
|
41
|
+
mediumEmphasis: Number(style.getPropertyValue("--v-medium-emphasis-opacity")),
|
|
42
|
+
contrast: (lighter + 0.05) / (darker + 0.05)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("the task transcript's user bubble, header time and footer follow the light and dark themes", {
|
|
47
|
+
skip: process.env.JSKIT_ASSISTANT_CORE_BROWSER_INTEGRATION !== "1", timeout: 90_000
|
|
48
|
+
}, async () => {
|
|
49
|
+
const vite = await startViteFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/responsive-assistant", import.meta.url)) });
|
|
50
|
+
const browser = await chromium.launch(createChromiumLaunchOptions());
|
|
51
|
+
try {
|
|
52
|
+
for (const theme of ["light", "dark"]) {
|
|
53
|
+
const page = await browser.newPage({ viewport: { width: 800, height: 900 } });
|
|
54
|
+
await page.goto(`${vite.baseURL}/?delivery=1&recovery=1&task=1${theme === "dark" ? "&dark=1" : ""}`);
|
|
55
|
+
await expect(page.locator(".v-application")).toHaveClass(new RegExp(`v-theme--${theme}`, "u"));
|
|
56
|
+
const bubble = page.locator(".assistant-transcript--task .assistant-transcript__message--user").filter({ hasText: "Question one" });
|
|
57
|
+
await expect(bubble).toBeVisible();
|
|
58
|
+
assert.equal(await bubble.evaluate((element) => getComputedStyle(element).backgroundColor), "rgba(126, 87, 194, 0.16)");
|
|
59
|
+
// The transcript's own headers carry no time, so the check places one where an application's header would.
|
|
60
|
+
const header = page.locator(".assistant-transcript__turn .assistant-transcript__message-header").first();
|
|
61
|
+
await header.evaluate((element) => {
|
|
62
|
+
const time = document.createElement("time");
|
|
63
|
+
for (const { name } of element.attributes) {
|
|
64
|
+
if (name.startsWith("data-v-")) time.setAttribute(name, "");
|
|
65
|
+
}
|
|
66
|
+
time.textContent = "06:02";
|
|
67
|
+
element.append(time);
|
|
68
|
+
});
|
|
69
|
+
const samples = {
|
|
70
|
+
"user bubble": [bubble.locator(".assistant-transcript__user-content"), 1],
|
|
71
|
+
"user footer": [bubble.locator(".assistant-transcript__message-footer time"), "medium"],
|
|
72
|
+
"assistant footer": [page.locator(".assistant-transcript__message-footer--assistant time").first(), "medium"],
|
|
73
|
+
"header time": [header.locator("time"), "medium"]
|
|
74
|
+
};
|
|
75
|
+
for (const [name, [locator, alpha]] of Object.entries(samples)) {
|
|
76
|
+
const text = await locator.evaluate(readText);
|
|
77
|
+
assert.deepEqual(text.color.slice(0, 3), text.onSurface, `${theme} ${name}: the colour is the theme's on-surface`);
|
|
78
|
+
assert.equal(text.color[3], alpha === "medium" ? text.mediumEmphasis : alpha, `${theme} ${name}: the opacity`);
|
|
79
|
+
assert.ok(text.contrast >= 4.5, `${theme} ${name}: contrast ${text.contrast.toFixed(2)}:1 is under 4.5:1`);
|
|
80
|
+
}
|
|
81
|
+
await page.close();
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
await browser.close();
|
|
85
|
+
await stopProcess(vite);
|
|
86
|
+
}
|
|
87
|
+
});
|