@jskit-ai/assistant-core 0.1.204 → 0.1.206
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 +5 -5
- package/fixtures/responsive-assistant/App.vue +12 -2
- package/fixtures/responsive-assistant/ConversationFixture.vue +79 -2
- package/package.json +19 -9
- package/src/client/conversation/AssistantConversationElement.vue +107 -41
- package/src/client/conversation/AssistantTranscript.vue +192 -18
- package/src/server/conversation/claudeClient.js +2 -1
- package/src/server/conversation/claudeTurn.js +16 -7
- package/src/server/conversation/codexConversations.js +94 -0
- package/src/server/conversation/codexProvider.js +15 -5
- package/src/server/conversation/codexProviderOwner.js +70 -47
- package/src/server/conversation/codexProviderSelection.js +5 -2
- package/src/server/conversation/codexRuntimeLifecycle.js +2 -1
- package/src/server/conversation/codexTurn.js +10 -1
- package/src/server/conversation/codexTurnControl.js +1 -1
- package/src/server/conversation/codexTurnRecovery.js +21 -8
- package/src/server/conversation/openCodeRuntime.js +107 -64
- package/src/server/conversation/openCodeTurn.js +33 -16
- package/src/server/conversation/providers/api.js +1 -0
- package/src/server/conversation/providers/claudeDriver.js +13 -0
- package/src/server/conversation/providers/codex.js +90 -47
- package/src/server/conversation/providers/codexDriver.js +4 -1
- package/src/server/conversation/providers/opencodeDriver.js +3 -0
- package/src/server/conversation/runtime.js +32 -5
- package/src/server/hosts/codexEvents.js +15 -0
- package/src/server/hosts/codexTurn.js +5 -0
- package/src/server/hosts/openCodeClient.js +4 -0
- package/src/server/lib/aiConnectionClient.js +9 -11
- package/test/aiConnectionClient.test.js +209 -0
- package/test/assistantScroll.browser.test.js +437 -9
- package/test/claudeClient.test.js +41 -0
- package/test/claudeConversationSession.test.js +78 -0
- package/test/claudeTurn.test.js +18 -0
- package/test/codexConversationSession.test.js +318 -0
- package/test/codexTurn.test.js +203 -2
- package/test/conversationConfiguration.test.js +27 -0
- package/test/conversationReplacement.test.js +76 -1
- package/test/conversationRuntime.test.js +173 -4
- package/test/conversationSession.test.js +139 -0
- package/test/conversationTools.test.js +17 -3
- package/test/opencodeConversationSession.test.js +200 -1
- package/test/packageMetadata.test.js +75 -0
- package/test/serviceToolCatalog.test.js +31 -0
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
"build": "vite build"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/assistant-core": "0.1.
|
|
12
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
13
|
-
"@jskit-ai/kernel": "0.1.
|
|
11
|
+
"@jskit-ai/assistant-core": "0.1.206",
|
|
12
|
+
"@jskit-ai/http-runtime": "0.1.226",
|
|
13
|
+
"@jskit-ai/kernel": "0.1.228",
|
|
14
14
|
"@mdi/js": "^7.4.47",
|
|
15
15
|
"vue": "^3.5.13",
|
|
16
16
|
"vuetify": "^4.0.0",
|
|
17
|
-
"@jskit-ai/connectors-core": "0.1.
|
|
18
|
-
"@jskit-ai/connectors-catalog": "0.1.
|
|
17
|
+
"@jskit-ai/connectors-core": "0.1.45",
|
|
18
|
+
"@jskit-ai/connectors-catalog": "0.1.45"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
@@ -4,8 +4,12 @@ import AssistantConversationElement from "../../src/client/conversation/Assistan
|
|
|
4
4
|
import { conversationTurnsFromMessages } from "../../src/shared/conversation/turns.js";
|
|
5
5
|
import { mergeConversationStream } from "../../src/shared/conversation/streaming.js";
|
|
6
6
|
const draft = ref("");
|
|
7
|
-
const
|
|
7
|
+
const error = ref("");
|
|
8
|
+
const reloads = ref(0);
|
|
8
9
|
const query = new URLSearchParams(location.search);
|
|
10
|
+
const visible = ref(!query.has("initial-hidden"));
|
|
11
|
+
const retainWhenHidden = query.has("retain-hidden");
|
|
12
|
+
const mounted = ref(true);
|
|
9
13
|
const controls = query.has("controls");
|
|
10
14
|
const deferredHistory = query.has("history");
|
|
11
15
|
const pagedHistory = query.has("paged-history");
|
|
@@ -52,12 +56,16 @@ function finishHistory(error = "") {
|
|
|
52
56
|
const adapter = computed(() => ({
|
|
53
57
|
conversation: {
|
|
54
58
|
visible: visible.value,
|
|
59
|
+
retainWhenHidden,
|
|
60
|
+
error: error.value,
|
|
61
|
+
errorReloadable: true,
|
|
55
62
|
turns: mergeConversationStream(conversationTurnsFromMessages(messages.value), { messages: [liveMessage.value] }), scrollKey: scope.value,
|
|
56
63
|
hasMoreBefore: hasMoreBefore.value, loadingMore: loadingMore.value, loadMoreError: loadMoreError.value
|
|
57
64
|
},
|
|
58
65
|
composer: { draft: draft.value, canSend: Boolean(draft.value.trim()), rows: 2 },
|
|
59
66
|
actions: {
|
|
60
67
|
setDraft(value) { draft.value = value; },
|
|
68
|
+
reload() { reloads.value += 1; error.value = ""; },
|
|
61
69
|
submit() { draft.value = ""; },
|
|
62
70
|
loadMore({ complete }) {
|
|
63
71
|
historyRequests.value += 1;
|
|
@@ -81,6 +89,8 @@ const adapter = computed(() => ({
|
|
|
81
89
|
<button @click="visible = !visible">Toggle visibility</button>
|
|
82
90
|
<button @click="scope = scope === 'one' ? 'two' : 'one'">Change conversation</button>
|
|
83
91
|
<button @click="messages.push({ id: `added-${messages.length}`, role: 'assistant', text: 'New reply. '.repeat(30), status: 'completed' })">Append reply</button>
|
|
92
|
+
<button v-if="query.has('update-errors')" @click="error = 'Conversation updates are temporarily unavailable. Reload to reconnect; the assistant may still be working.'">Show update error</button>
|
|
93
|
+
<button v-if="retainWhenHidden" @click="mounted = !mounted">Toggle mount</button>
|
|
84
94
|
</nav>
|
|
85
95
|
<nav v-if="deferredHistory">
|
|
86
96
|
<button :disabled="!loadingMore" @click="finishHistory()">Complete history load</button>
|
|
@@ -88,7 +98,7 @@ const adapter = computed(() => ({
|
|
|
88
98
|
<button @click="hasMoreBefore = false">Exhaust history</button>
|
|
89
99
|
<output>History requests: {{ historyRequests }}</output>
|
|
90
100
|
</nav>
|
|
91
|
-
<AssistantConversationElement v-show="visible" :adapter="adapter" class="assistant-responsive-fixture__conversation" />
|
|
101
|
+
<AssistantConversationElement v-if="mounted" v-show="visible" :adapter="adapter" :data-error-reloads="reloads" class="assistant-responsive-fixture__conversation" />
|
|
92
102
|
</main>
|
|
93
103
|
</v-main>
|
|
94
104
|
</v-app>
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { computed, reactive, ref } from "vue";
|
|
3
|
+
import { createAssistantMessageDelivery } from "../../src/client/conversation/messageDelivery.js";
|
|
3
4
|
import AssistantConversationElement from "../../src/client/conversation/AssistantConversationElement.vue";
|
|
4
5
|
import { useAssistantAttachments } from "../../src/client/conversation/useAssistantAttachments.js";
|
|
5
6
|
import { useAssistantSuggestions } from "../../src/client/conversation/useAssistantSuggestions.js";
|
|
6
7
|
const phase = ref("idle");
|
|
8
|
+
const previewFixture = new URLSearchParams(location.search).has("preview");
|
|
9
|
+
const temporaryWords = ref("");
|
|
10
|
+
const temporaryPresent = ref(false);
|
|
11
|
+
const temporaryEditing = ref(false);
|
|
12
|
+
const newerWords = ref("");
|
|
13
|
+
const newerPresent = ref(false);
|
|
14
|
+
const newerEditing = ref(false);
|
|
15
|
+
const previewCanTake = ref(true);
|
|
16
|
+
const previewDelivery = createAssistantMessageDelivery();
|
|
7
17
|
const avatarFixture = new URLSearchParams(location.search).has("avatar");
|
|
18
|
+
const avatarToolsFixture = new URLSearchParams(location.search).has("avatarTools");
|
|
8
19
|
const showAvatar = ref(avatarFixture);
|
|
9
20
|
const avatarSize = ref("compact");
|
|
10
21
|
const short = ref(avatarFixture);
|
|
@@ -63,8 +74,57 @@ const goalState = reactive({
|
|
|
63
74
|
resume() { goal.value = { ...goal.value, status: "active", sampledAt: Date.now() }; }
|
|
64
75
|
});
|
|
65
76
|
const adapter = reactive({
|
|
77
|
+
delivery: previewFixture ? previewDelivery : undefined,
|
|
66
78
|
conversation: {
|
|
67
79
|
assistantLabel,
|
|
80
|
+
previewMessages: computed(() => {
|
|
81
|
+
const pending = adapter.conversation.previewMessage;
|
|
82
|
+
const previews = pending ? [pending] : [];
|
|
83
|
+
if (newerPresent.value) previews.push({ id: "newer", text: newerWords.value, actions: {
|
|
84
|
+
canDiscard: previewCanTake.value,
|
|
85
|
+
canEdit: previewCanTake.value && !temporaryPresent.value,
|
|
86
|
+
editing: newerEditing.value,
|
|
87
|
+
discard: id => { if (id === "newer" && previewCanTake.value) newerPresent.value = false; },
|
|
88
|
+
edit: id => { if (id === "newer" && previewCanTake.value && !temporaryPresent.value) newerEditing.value = true; },
|
|
89
|
+
update: (id, text) => { if (id === "newer" && newerEditing.value) newerWords.value = text; },
|
|
90
|
+
send: newerEditing.value ? id => {
|
|
91
|
+
if (id !== "newer" || !previewCanTake.value || !newerWords.value.trim()) return;
|
|
92
|
+
adapter.conversation.turns = [...adapter.conversation.turns, { turnId: id,
|
|
93
|
+
user: { messageId: id, role: "user", text: newerWords.value } }];
|
|
94
|
+
newerPresent.value = false;
|
|
95
|
+
} : null,
|
|
96
|
+
canSend: previewCanTake.value && Boolean(newerWords.value.trim()), sending: false
|
|
97
|
+
} });
|
|
98
|
+
return previews;
|
|
99
|
+
}),
|
|
100
|
+
previewMessage: computed(() => temporaryPresent.value ? {
|
|
101
|
+
id: "temporary", text: temporaryWords.value,
|
|
102
|
+
actions: {
|
|
103
|
+
canDiscard: previewCanTake.value,
|
|
104
|
+
canEdit: previewCanTake.value,
|
|
105
|
+
editing: temporaryEditing.value,
|
|
106
|
+
canSend: previewCanTake.value && Boolean(temporaryWords.value.trim()),
|
|
107
|
+
sending: false,
|
|
108
|
+
discard(id) {
|
|
109
|
+
if (id !== "temporary" || !previewCanTake.value) return;
|
|
110
|
+
temporaryPresent.value = false;
|
|
111
|
+
if (previewDelivery.find(id)?.status === "failed") previewDelivery.cancel(id);
|
|
112
|
+
},
|
|
113
|
+
edit(id) {
|
|
114
|
+
if (id !== "temporary" || !previewCanTake.value) return;
|
|
115
|
+
temporaryEditing.value = true;
|
|
116
|
+
if (previewDelivery.find(id)?.status === "failed") previewDelivery.cancel(id);
|
|
117
|
+
},
|
|
118
|
+
update(id, text) {
|
|
119
|
+
if (id === "temporary" && previewCanTake.value) temporaryWords.value = text;
|
|
120
|
+
},
|
|
121
|
+
send(id) {
|
|
122
|
+
if (id !== "temporary" || !previewCanTake.value || !temporaryWords.value.trim()) return;
|
|
123
|
+
adapter.conversation.turns = [{ turnId: id, user: { messageId: id, role: "user", text: temporaryWords.value } }];
|
|
124
|
+
temporaryPresent.value = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} : null),
|
|
68
128
|
turns: attribution ? [
|
|
69
129
|
{ turnId: "one", assistantLabel: "First assistant", assistantDetails: "First model",
|
|
70
130
|
assistant: { role: "assistant", text: "First answer" } },
|
|
@@ -76,7 +136,7 @@ const adapter = reactive({
|
|
|
76
136
|
assistant: { messageId: `answer-${index}`, outputId: `output-${index}`, role: "assistant",
|
|
77
137
|
text: `Support answer ${index + 1}: Keep your recovery email available.\n\nRead the **account instructions** and follow the [recovery link](https://example.com/help). A long message wraps inside this narrow conversation without taking space from the composer.` }
|
|
78
138
|
})) : [],
|
|
79
|
-
visible: true, scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture"
|
|
139
|
+
visible: true, reloadable: avatarFixture, scrollKey: "fixture", welcomeMessage: "Composer responsiveness fixture"
|
|
80
140
|
},
|
|
81
141
|
composer: {
|
|
82
142
|
draft,
|
|
@@ -111,12 +171,21 @@ const adapter = reactive({
|
|
|
111
171
|
<button v-if="supportEnabled" @click="customActivity = !customActivity">Custom activity</button>
|
|
112
172
|
<button v-if="capabilitiesEnabled" @click="finishUploads">Finish uploads</button>
|
|
113
173
|
<button @click="goalEnabled = !goalEnabled">Toggle goals</button>
|
|
174
|
+
<template v-if="previewFixture">
|
|
175
|
+
<button @click="temporaryWords = 'Exact temporary words'; temporaryPresent = true; temporaryEditing = false">Add temporary message</button>
|
|
176
|
+
<button @click="newerWords = 'Keep newer live words'; newerPresent = true; newerEditing = false">Add newer temporary message</button>
|
|
177
|
+
<button @click="previewCanTake = !previewCanTake">Toggle preview eligibility</button>
|
|
178
|
+
<button @click="previewDelivery.send({ message: temporaryWords, request: { text: temporaryWords, steer: true } }, { messageId: 'temporary', deliver: async () => ({ ok: false, error: 'No active turn; explicit Edit may author a new message.' }) })">Reject preview</button>
|
|
179
|
+
<button @click="adapter.conversation.turns = [{ turnId: 'temporary', user: { messageId: 'temporary', role: 'user', text: temporaryWords } }]">Accept preview</button>
|
|
180
|
+
<button @click="adapter.conversation.turns = [{ turnId: 'temporary', user: { messageId: 'temporary', role: 'user', text: temporaryWords, receipt: false } }]">Unadmitted preview</button>
|
|
181
|
+
</template>
|
|
114
182
|
<template v-if="avatarFixture">
|
|
115
183
|
<button @click="showAvatar = !showAvatar">Toggle avatar slot</button>
|
|
116
184
|
<button @click="short = !short">Resize height</button>
|
|
117
185
|
<button @click="showQuestions = !showQuestions">Toggle questions</button>
|
|
118
186
|
<button @click="attachments.status.value = 'An upload failed. Retry or remove it.'">Show attachment error</button>
|
|
119
|
-
<
|
|
187
|
+
<select v-model="avatarSize" aria-label="Avatar preference"><option v-for="size in ['hidden', 'compact', 'standard', 'large']" :key="size" :value="size">{{ size }}</option></select>
|
|
188
|
+
<output class="avatar-preference">Avatar requested: {{ avatarSize }}</output>
|
|
120
189
|
</template>
|
|
121
190
|
<output>{{ phase }}; submitted {{ submissions }}</output>
|
|
122
191
|
<output v-if="capabilitiesEnabled">Model: {{ appliedModel }}; sent files: {{ attachmentReceipts.map(item => item.fileName).join(', ') }}</output>
|
|
@@ -124,6 +193,12 @@ const adapter = reactive({
|
|
|
124
193
|
<main class="fixture" :class="{ narrow, 'fixture--avatar': avatarFixture, 'fixture--short': short }">
|
|
125
194
|
<AssistantConversationElement v-model:avatar-size="avatarSize" :adapter="adapter">
|
|
126
195
|
<template v-if="showAvatar" #avatar><svg viewBox="0 0 100 100" role="img" aria-label="Support assistant avatar"><circle cx="50" cy="50" r="45" fill="#6750a4" /><path d="M30 62Q50 80 70 62M35 40h1m28 0h1" fill="none" stroke="white" stroke-width="6" stroke-linecap="round" /></svg></template>
|
|
196
|
+
<template v-if="avatarToolsFixture" #avatar-tools="scope">
|
|
197
|
+
<button class="fixture-avatar-tool" aria-label="Avatar tool" :data-size="scope.size" :data-height="scope.height">Tool</button>
|
|
198
|
+
</template>
|
|
199
|
+
<template v-if="avatarToolsFixture" #avatar-control="scope">
|
|
200
|
+
<button class="fixture-avatar-tool" aria-label="Retained avatar control" :data-size="scope.size">Mic</button>
|
|
201
|
+
</template>
|
|
127
202
|
<template v-if="customActivity" #activity="{ activity }"><strong>Custom activity: {{ activity.label }}</strong></template>
|
|
128
203
|
<template #composer-feedback><p v-if="feedback" class="feedback" role="status">Describe what you want to change.</p></template>
|
|
129
204
|
</AssistantConversationElement>
|
|
@@ -134,9 +209,11 @@ const adapter = reactive({
|
|
|
134
209
|
<style scoped>
|
|
135
210
|
.controls { display: flex; flex-wrap: wrap; gap: 12px; padding: 12px; }
|
|
136
211
|
.controls button { border: 1px solid; padding: 4px; }
|
|
212
|
+
.avatar-preference { flex: 0 0 14rem; }
|
|
137
213
|
.fixture { height: 70vh; width: min(900px, 100%); margin: auto; }
|
|
138
214
|
.fixture.narrow { width: min(320px, 100%); }
|
|
139
215
|
.fixture--avatar { width: min(320px, 100%); }
|
|
216
|
+
.fixture-avatar-tool { flex: 0 0 48px; min-width: 48px; min-height: 48px; }
|
|
140
217
|
.fixture--short { height: 420px; }
|
|
141
218
|
.feedback { flex: 1 1 24rem; min-width: 0; margin: 0; }
|
|
142
219
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/assistant-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.206",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test --test-concurrency=1"
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"./server/codex-provider": "./src/server/hosts/codexProvider.js",
|
|
33
33
|
"./server/native-history": "./src/server/hosts/nativeHistory.js",
|
|
34
34
|
"./testing/native-codex": "./src/testing/nativeCodex.js",
|
|
35
|
-
"./testing/native-opencode": "./src/testing/nativeOpenCode.js"
|
|
35
|
+
"./testing/native-opencode": "./src/testing/nativeOpenCode.js",
|
|
36
|
+
"./server/codex-client": "./src/server/conversation/codexClient.js",
|
|
37
|
+
"./client/conversation-submit": "./src/client/conversation/submitText.js"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@ai-sdk/anthropic": "^4.0.53",
|
|
@@ -45,8 +47,8 @@
|
|
|
45
47
|
"@ai-sdk/perplexity": "^4.0.43",
|
|
46
48
|
"@ai-sdk/togetherai": "^3.0.49",
|
|
47
49
|
"@ai-sdk/xai": "^4.0.58",
|
|
48
|
-
"@jskit-ai/resource-core": "0.1.
|
|
49
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
50
|
+
"@jskit-ai/resource-core": "0.1.170",
|
|
51
|
+
"@jskit-ai/resource-crud-core": "0.1.170",
|
|
50
52
|
"@mdi/js": "^7.4.47",
|
|
51
53
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
52
54
|
"ai": "^7.0.101",
|
|
@@ -55,8 +57,8 @@
|
|
|
55
57
|
"ws": "^8.20.1"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
59
|
-
"@jskit-ai/kernel": "0.1.
|
|
60
|
+
"@jskit-ai/http-runtime": "0.1.226",
|
|
61
|
+
"@jskit-ai/kernel": "0.1.228",
|
|
60
62
|
"vue": "^3.5.13",
|
|
61
63
|
"vuetify": "^4.0.0"
|
|
62
64
|
},
|
|
@@ -126,15 +128,15 @@
|
|
|
126
128
|
},
|
|
127
129
|
{
|
|
128
130
|
"subpath": "./server/codex-events",
|
|
129
|
-
"summary": "Advanced native host: Codex event
|
|
131
|
+
"summary": "Advanced native host: Original Codex event classification, text, status and identity helpers, thread-history projections and context-refresh facts."
|
|
130
132
|
},
|
|
131
133
|
{
|
|
132
134
|
"subpath": "./server/codex-turn",
|
|
133
|
-
"summary": "Advanced native host: Original Codex run, receipt, observation, goal, steering, control, scoped-turn and renewal coordination."
|
|
135
|
+
"summary": "Advanced native host: Original detached-turn watcher and status predicates, plus Codex run, receipt, observation, goal, steering, control, scoped-turn and renewal coordination."
|
|
134
136
|
},
|
|
135
137
|
{
|
|
136
138
|
"subpath": "./server/opencode-client",
|
|
137
|
-
"summary": "Advanced native host: OpenCode native history retirement
|
|
139
|
+
"summary": "Advanced native host: Original OpenCode loopback client, bounded response and assistant text helpers, plus native history retirement."
|
|
138
140
|
},
|
|
139
141
|
{
|
|
140
142
|
"subpath": "./server/claude-history",
|
|
@@ -187,6 +189,14 @@
|
|
|
187
189
|
{
|
|
188
190
|
"subpath": "./testing/native-opencode",
|
|
189
191
|
"summary": "Testing: Original OpenCode loopback client for isolated host integration tests; tests own endpoints, authorization and cleanup."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"subpath": "./server/codex-client",
|
|
195
|
+
"summary": "Advanced native host: Original Codex JSON-RPC client and endpoint socket-path helper; hosts retain authorization, transport limits and native lifecycle coordination."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"subpath": "./client/conversation-submit",
|
|
199
|
+
"summary": "Optional UI: Original text submission helper forwards companion text through the application draft and canonical send action; also exported by ./client/conversation."
|
|
190
200
|
}
|
|
191
201
|
],
|
|
192
202
|
"capabilities": []
|
|
@@ -1,32 +1,74 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section ref="container" class="assistant-conversation" :aria-label="label">
|
|
3
|
-
<section
|
|
4
|
-
|
|
3
|
+
<section
|
|
4
|
+
v-if="$slots.avatar" class="assistant-conversation__avatar"
|
|
5
|
+
:class="{ 'assistant-conversation__avatar--hidden': avatarSize === 'hidden' }" aria-label="Conversation avatar"
|
|
6
|
+
>
|
|
7
|
+
<div v-show="avatarSize === 'hidden'" ref="avatarControls" class="assistant-conversation__avatar-controls">
|
|
8
|
+
<slot name="avatar-control" :size="avatarSize" :height="avatarHeight" />
|
|
5
9
|
<v-btn
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
:icon="mdiAccountCircleOutline" size="small"
|
|
11
|
+
variant="text" :width="44" :min-width="44" :min-height="44"
|
|
12
|
+
class="assistant-conversation__avatar-size"
|
|
13
|
+
aria-label="Show avatar" title="Show avatar"
|
|
14
|
+
:aria-expanded="avatarSize !== 'hidden'" @click="toggleAvatar"
|
|
15
|
+
>
|
|
16
|
+
<span class="assistant-conversation__avatar-size-disc">
|
|
17
|
+
<v-icon size="20" :icon="mdiAccountCircleOutline" />
|
|
18
|
+
</span>
|
|
19
|
+
</v-btn>
|
|
9
20
|
</div>
|
|
10
|
-
<
|
|
11
|
-
<v-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
<div v-show="avatarSize !== 'hidden'" class="assistant-conversation__avatar-presentation">
|
|
22
|
+
<div v-if="avatarSize !== 'hidden'" class="assistant-conversation__avatar-visual" :style="{ height: `${avatarHeight}px`, width: `${avatarHeight}px` }">
|
|
23
|
+
<slot name="avatar" :size="avatarSize" :height="avatarHeight" />
|
|
24
|
+
</div>
|
|
25
|
+
<div class="assistant-conversation__avatar-tools">
|
|
26
|
+
<slot name="avatar-tools" :size="avatarSize" :height="avatarHeight" />
|
|
27
|
+
<v-btn
|
|
28
|
+
:icon="mdiMinus" size="small" variant="text" color="primary"
|
|
29
|
+
:width="40" :min-width="40" :min-height="44" class="assistant-conversation__avatar-size"
|
|
30
|
+
aria-label="Minimise avatar" title="Minimise avatar" :aria-expanded="true"
|
|
31
|
+
@click="toggleAvatar"
|
|
32
|
+
>
|
|
33
|
+
<span class="assistant-conversation__avatar-size-disc assistant-conversation__avatar-size-disc--minimise">
|
|
34
|
+
<v-icon size="20" :icon="mdiMinus" />
|
|
35
|
+
</span>
|
|
36
|
+
</v-btn>
|
|
37
|
+
</div>
|
|
20
38
|
</div>
|
|
21
39
|
</section>
|
|
22
40
|
<AssistantTranscript
|
|
23
|
-
v-bind="conversation" :working="working" class="assistant-conversation__transcript"
|
|
41
|
+
v-bind="conversation" :preview-message="adapter.conversation.previewMessage || null" :working="working" class="assistant-conversation__transcript"
|
|
24
42
|
@load-more="adapter.actions?.loadMore?.($event)" @reload="adapter.actions?.reload?.()"
|
|
25
43
|
@resend-turn="resend($event)" @cancel-turn="cancel($event)"
|
|
26
44
|
@check-delivery="adapter.actions?.checkDelivery?.($event)"
|
|
27
45
|
@edit-turn="edit($event)" @link-click="adapter.actions?.openLink?.($event)"
|
|
28
46
|
>
|
|
29
47
|
<template v-for="name in transcriptSlots" #[name]="scope"><slot :name="name" v-bind="scope" /></template>
|
|
48
|
+
<template #user-message-actions="scope">
|
|
49
|
+
<div v-if="scope.turn.preview && scope.turn.previewMessage?.actions" class="assistant-conversation__preview-actions">
|
|
50
|
+
<v-btn
|
|
51
|
+
v-if="scope.turn.previewMessage.actions.discard" :icon="mdiClose" size="small" variant="text"
|
|
52
|
+
aria-label="Discard unsent message" title="Discard unsent message"
|
|
53
|
+
:disabled="!scope.turn.previewMessage.actions.canDiscard"
|
|
54
|
+
@click="scope.turn.previewMessage.actions.discard(scope.turn.turnId)"
|
|
55
|
+
/>
|
|
56
|
+
<v-btn
|
|
57
|
+
v-if="scope.turn.previewMessage.actions.edit" :icon="mdiPencil" size="small" variant="text"
|
|
58
|
+
aria-label="Edit unsent message" title="Edit unsent message"
|
|
59
|
+
:disabled="!scope.turn.previewMessage.actions.canEdit"
|
|
60
|
+
@click="scope.turn.previewMessage.actions.edit(scope.turn.turnId)"
|
|
61
|
+
/>
|
|
62
|
+
<v-btn
|
|
63
|
+
v-if="scope.turn.previewMessage.actions.send" size="small" color="primary"
|
|
64
|
+
:disabled="!scope.turn.previewMessage.actions.canSend"
|
|
65
|
+
@click="scope.turn.previewMessage.actions.send(scope.turn.turnId)"
|
|
66
|
+
>
|
|
67
|
+
{{ scope.turn.previewMessage.actions.sending ? 'Sending…' : 'Send' }}
|
|
68
|
+
</v-btn>
|
|
69
|
+
</div>
|
|
70
|
+
<slot name="user-message-actions" v-bind="scope" />
|
|
71
|
+
</template>
|
|
30
72
|
</AssistantTranscript>
|
|
31
73
|
<div v-if="adapter.goal && adapter.goal.enabled !== false" class="assistant-conversation__goal">
|
|
32
74
|
<AssistantGoalControl :state="adapter.goal" />
|
|
@@ -99,10 +141,6 @@
|
|
|
99
141
|
@update:model-value="updateConfiguration({ ...configuration, [field.name]: $event })"
|
|
100
142
|
/>
|
|
101
143
|
</slot>
|
|
102
|
-
<v-btn
|
|
103
|
-
v-if="$slots.avatar" ref="avatarControl" class="assistant-conversation__avatar-size" aria-label="Avatar size" :title="`Avatar size: ${avatarSize}`"
|
|
104
|
-
aria-haspopup="menu" :aria-expanded="avatarMenuOpen" :icon="mdiAccountCircleOutline" size="small" variant="text"
|
|
105
|
-
/>
|
|
106
144
|
<slot name="composer-tools" :adapter="adapter" />
|
|
107
145
|
<template #feedback>
|
|
108
146
|
<p v-if="attachmentsEnabled && adapter.attachments.status" role="alert" class="assistant-conversation__attachment-error">{{ adapter.attachments.status }}</p>
|
|
@@ -116,7 +154,7 @@
|
|
|
116
154
|
</template>
|
|
117
155
|
<script setup>
|
|
118
156
|
import { computed, onBeforeUnmount, onMounted, onUpdated, ref, useId, useSlots, watch } from "vue";
|
|
119
|
-
import { mdiAccountCircleOutline, mdiPaperclip } from "@mdi/js";
|
|
157
|
+
import { mdiAccountCircleOutline, mdiClose, mdiMinus, mdiPaperclip, mdiPencil } from "@mdi/js";
|
|
120
158
|
import AssistantTranscript from "./AssistantTranscript.vue";
|
|
121
159
|
import AssistantPromptInput from "./AssistantPromptInput.vue";
|
|
122
160
|
import AssistantComposerActions from "./AssistantComposerActions.vue";
|
|
@@ -133,17 +171,22 @@ const props = defineProps({
|
|
|
133
171
|
configurationFields: { type: Array, default: () => [] },
|
|
134
172
|
configurationMode: { type: String, default: "hidden", validator: (value) => ["hidden", "readonly", "editable"].includes(value) }
|
|
135
173
|
});
|
|
136
|
-
defineEmits(["update:avatarSize"]);
|
|
174
|
+
const emit = defineEmits(["update:avatarSize"]);
|
|
137
175
|
const slots = useSlots();
|
|
138
176
|
const container = ref(null);
|
|
139
177
|
const avatarControls = ref(null);
|
|
140
|
-
const
|
|
141
|
-
|
|
178
|
+
const visibleAvatarSize = ref("compact");
|
|
179
|
+
watch(() => props.avatarSize, (size) => {
|
|
180
|
+
if (size !== "hidden") visibleAvatarSize.value = size;
|
|
181
|
+
}, { immediate: true });
|
|
182
|
+
function toggleAvatar() {
|
|
183
|
+
emit("update:avatarSize", props.avatarSize === "hidden" ? visibleAvatarSize.value : "hidden");
|
|
184
|
+
}
|
|
142
185
|
const avatarSizes = [
|
|
143
|
-
{
|
|
144
|
-
{
|
|
145
|
-
{
|
|
146
|
-
{
|
|
186
|
+
{ value: "hidden", height: 0 },
|
|
187
|
+
{ value: "compact", height: 96 },
|
|
188
|
+
{ value: "standard", height: 112 },
|
|
189
|
+
{ value: "large", height: 176 }
|
|
147
190
|
];
|
|
148
191
|
const avatarSpace = ref(0);
|
|
149
192
|
const avatarHeight = computed(() => Math.min(avatarSpace.value, avatarSizes.find(size => size.value === props.avatarSize).height));
|
|
@@ -198,8 +241,8 @@ const delivery = ref(null);
|
|
|
198
241
|
const statusId = `assistant-status-${useId()}`;
|
|
199
242
|
const conversation = computed(() => {
|
|
200
243
|
const saved = props.adapter.conversation;
|
|
201
|
-
if (!props.adapter.delivery && !saved.previewMessage && !saved.interimReply) return saved;
|
|
202
|
-
const { previewMessage, interimReply, ...display } = saved;
|
|
244
|
+
if (!props.adapter.delivery && !saved.previewMessage && !saved.previewMessages?.length && !saved.interimReply) return saved;
|
|
245
|
+
const { previewMessage, previewMessages, interimReply, ...display } = saved;
|
|
203
246
|
let turns = props.adapter.delivery ? props.adapter.delivery.turns(saved.turns || []) : saved.turns || [];
|
|
204
247
|
if (interimReply?.text && interimReply.turnId) {
|
|
205
248
|
const assistant = { ...interimReply, messageId: interimReply.id, role: "assistant" };
|
|
@@ -212,13 +255,30 @@ const conversation = computed(() => {
|
|
|
212
255
|
(message.outputId || message.messageId) !== (interimReply.outputId || interimReply.id)).concat(assistant) } : {})
|
|
213
256
|
});
|
|
214
257
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
258
|
+
// This list projects existing capture/pending owners; it never queues delivery.
|
|
259
|
+
const previews = Array.isArray(previewMessages) ? previewMessages : previewMessage ? [previewMessage] : [];
|
|
260
|
+
for (const preview of previews) {
|
|
261
|
+
const messageId = String(preview?.messageId || preview?.id || "").trim();
|
|
262
|
+
const localDelivery = messageId && props.adapter.delivery?.find?.(messageId);
|
|
263
|
+
if ((!localDelivery || localDelivery.status === "failed") && preview?.actions?.send) {
|
|
264
|
+
// Keep one unadmitted bubble and its real status until explicit Edit/discard.
|
|
265
|
+
// A receipt:false user row is not acknowledgement of the pending words.
|
|
266
|
+
turns = turns.map(turn => {
|
|
267
|
+
const failed = turn.optimistic?.id === messageId && turn.optimistic.status === "failed";
|
|
268
|
+
const unadmitted = turn.user?.receipt === false && String(turn.user.messageId || turn.user.id || "") === messageId;
|
|
269
|
+
if (!failed && !unadmitted) return turn;
|
|
270
|
+
return { ...turn, preview: true, previewMessage: preview, user: { ...turn.user, text: String(preview.text || "") } };
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const alreadyShown = messageId && (localDelivery || turns.some(turn => [
|
|
274
|
+
turn.system, turn.user, turn.assistant, ...(turn.thinking || []), ...(turn.commentary || []), ...(turn.messages || [])
|
|
275
|
+
].some(message => String(message?.messageId || message?.id || "") === messageId &&
|
|
276
|
+
(message.receipt !== false || turn.preview))));
|
|
277
|
+
if (messageId && (preview?.text || preview?.actions?.editing) && !alreadyShown) {
|
|
278
|
+
const user = { messageId, role: "user", text: String(preview.text || "") };
|
|
279
|
+
turns = [...turns, { turnId: messageId, user, messages: [user], preview: true, previewMessage: preview,
|
|
280
|
+
optimistic: { id: messageId, status: "pending" } }];
|
|
281
|
+
}
|
|
222
282
|
}
|
|
223
283
|
// Transcription and interim replies are display only. The receipt watcher
|
|
224
284
|
// receives canonical turns, never these presentation or delivery overlays.
|
|
@@ -327,10 +387,16 @@ function updateConfiguration(value) {
|
|
|
327
387
|
defineExpose({ focus: () => input.value?.focus(), submit, stop });
|
|
328
388
|
</script>
|
|
329
389
|
<style scoped>
|
|
330
|
-
.assistant-
|
|
331
|
-
.assistant-
|
|
332
|
-
.assistant-conversation__avatar
|
|
333
|
-
.assistant-conversation__avatar-
|
|
390
|
+
.assistant-conversation__preview-actions { display: flex; justify-content: flex-end; }
|
|
391
|
+
.assistant-conversation { position: relative; display: flex; flex-direction: column; min-height: 0; min-width: 0; height: 100%; gap: 0; container: assistant-conversation / inline-size; }
|
|
392
|
+
.assistant-conversation__avatar { position: absolute; top: 0; right: 0; z-index: 3; max-width: 100%; min-width: 0; }
|
|
393
|
+
.assistant-conversation__avatar-controls { position: absolute; top: 0; right: 100%; display: flex; align-items: center; justify-content: flex-end; min-height: 44px; }
|
|
394
|
+
.assistant-conversation__avatar--hidden .assistant-conversation__avatar-controls { right: 0; }
|
|
395
|
+
.assistant-conversation__avatar-size { min-width: 44px; min-height: 44px; }
|
|
396
|
+
.assistant-conversation__avatar-size-disc { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; background: rgba(var(--v-theme-on-surface), .08); }
|
|
397
|
+
.assistant-conversation__avatar-size-disc--minimise { background: rgba(var(--v-theme-primary), .12); }
|
|
398
|
+
.assistant-conversation__avatar-presentation { display: flex; flex-direction: column; align-items: center; min-width: 0; }
|
|
399
|
+
.assistant-conversation__avatar-tools { display: flex; align-items: center; gap: 0; }
|
|
334
400
|
.assistant-conversation__avatar-visual { display: flex; justify-content: center; min-height: 0; overflow: hidden; }
|
|
335
401
|
.assistant-conversation__avatar-visual :deep(> *) { max-width: 100%; max-height: 100%; }
|
|
336
402
|
.assistant-conversation__goal { display: flex; justify-content: flex-end; flex: 0 0 auto; }
|