@docsector/docsector-reader 4.5.6 → 4.6.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.
- package/README.md +6 -6
- package/bin/docsector.js +1 -1
- package/package.json +3 -2
- package/src/ai-assistant/panel.js +53 -0
- package/src/ai-assistant/session.js +13 -2
- package/src/components/DAssistantPanel.vue +154 -13
- package/src/composables/useAssistant.js +44 -13
- package/src/i18n/helpers.js +10 -0
- package/src/i18n/languages/en-US.hjson +5 -0
- package/src/i18n/languages/pt-BR.hjson +5 -0
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
59
59
|
- 🔗 **Anchor Navigation** — Right-side source-ordered Table of Contents tree with stable scroll tracking, resize-safe drawer state, auto-scroll to the active section, and active-heading resolution based on the last heading that crossed the content threshold
|
|
60
60
|
- 🖱️ **Active Menu Item UX** — Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
|
|
61
61
|
- 🔎 **Search** — Menu search across all documentation content and tags
|
|
62
|
-
- 💬 **Assistant Chat UX Enhancements** — Long conversations keep focus on recent messages, load earlier history progressively, deduplicate repeated sources, preserve the assistant panel open state across reloads, include per-message copy actions, and show a floating quick return to the bottom
|
|
62
|
+
- 💬 **Assistant Chat UX Enhancements** — Long conversations keep focus on recent messages, load earlier history progressively, deduplicate repeated sources, preserve the assistant panel open state across reloads, include per-message copy actions and hover-revealed message times, and show a floating quick return to the bottom
|
|
63
63
|
- 📱 **Responsive** — Mobile-friendly with collapsible sidebar and drawers
|
|
64
64
|
- 🏷️ **Clickable Header Branding** — The configured `branding.logo` and `branding.name` render as a home link in the global header, aligned left on desktop with a compact mobile treatment
|
|
65
65
|
- 📚 **Book Tabs with Per-State Colors** — Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
|
|
@@ -301,7 +301,7 @@ Check `checks.discovery.webMcp.status` equals `"pass"`.
|
|
|
301
301
|
|
|
302
302
|
Docsector Reader can add an opt-in assistant panel for documentation Q&A. Users open it from the global header while reading pages and subpages; it is not a dedicated documentation route. The drawer posts to a same-origin Cloudflare Pages Function, and that function calls Cloudflare AI Search so secrets, rate-limit strategy, provider errors, and future auth stay server-side.
|
|
303
303
|
|
|
304
|
-
The panel is disabled by default. When enabled, desktop pages get a dedicated right-side assistant rail that can sit beside the table of contents on wide screens. Mobile uses a fullscreen dialog. Conversations restore at the latest message, reveal earlier history progressively in long chats, deduplicate repeated source links, preserve the panel open state across page reloads, and provide per-message copy actions.
|
|
304
|
+
The panel is disabled by default. When enabled, desktop pages get a dedicated right-side assistant rail that can sit beside the table of contents on wide screens. Mobile uses a fullscreen dialog. Conversations restore at the latest message, reveal earlier history progressively in long chats, deduplicate repeated source links, preserve the panel open state across page reloads, and provide per-message copy actions with hover-revealed message times.
|
|
305
305
|
|
|
306
306
|
### Configure
|
|
307
307
|
|
|
@@ -332,8 +332,8 @@ export default {
|
|
|
332
332
|
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
333
333
|
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
334
334
|
model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
335
|
-
retrievalType: '
|
|
336
|
-
maxResults:
|
|
335
|
+
retrievalType: 'vector',
|
|
336
|
+
maxResults: 10,
|
|
337
337
|
matchThreshold: 0.4,
|
|
338
338
|
contextExpansion: 1,
|
|
339
339
|
queryRewrite: { enabled: true },
|
|
@@ -886,8 +886,8 @@ export default {
|
|
|
886
886
|
instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
|
|
887
887
|
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
888
888
|
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
889
|
-
retrievalType: '
|
|
890
|
-
maxResults:
|
|
889
|
+
retrievalType: 'vector',
|
|
890
|
+
maxResults: 10,
|
|
891
891
|
stream: true
|
|
892
892
|
}
|
|
893
893
|
},
|
package/bin/docsector.js
CHANGED
|
@@ -24,7 +24,7 @@ const packageRoot = resolve(__dirname, '..')
|
|
|
24
24
|
const args = process.argv.slice(2)
|
|
25
25
|
const command = args[0]
|
|
26
26
|
|
|
27
|
-
const VERSION = '4.
|
|
27
|
+
const VERSION = '4.6.0'
|
|
28
28
|
const AUTHORING_SKILL_NAME = 'docsector-documentation-authoring'
|
|
29
29
|
const AUTHORING_SKILL_DESCRIPTION = 'Author Docsector documentation with Markdown, custom blocks, MCP, and WebMCP.'
|
|
30
30
|
const AUTHORING_SKILL_PUBLIC_PATH = `/.well-known/agent-skills/${AUTHORING_SKILL_NAME}/SKILL.md`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"url": "https://github.com/docsector/docsector-reader/issues"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"dev": "
|
|
60
|
+
"dev": "quasar dev",
|
|
61
|
+
"dev:pages": "npx wrangler pages dev dist/spa",
|
|
61
62
|
"build": "quasar build",
|
|
62
63
|
"lint": "eslint --ext .js,.vue ./",
|
|
63
64
|
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
|
@@ -2,6 +2,39 @@ export function hasAssistantMessageContent(message) {
|
|
|
2
2
|
return String(message?.content || '').trim().length > 0
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function getAssistantMessageTimestamp(message) {
|
|
6
|
+
const timestamp = Number(message?.timestamp)
|
|
7
|
+
if (Number.isFinite(timestamp) && timestamp > 0) {
|
|
8
|
+
return timestamp
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const id = String(message?.id || '')
|
|
12
|
+
const match = id.match(/^(?:user|assistant)-(\d{12,})-/)
|
|
13
|
+
if (!match) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const legacyTimestamp = Number(match[1])
|
|
18
|
+
return Number.isFinite(legacyTimestamp) && legacyTimestamp > 0 ? legacyTimestamp : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatAssistantMessageTime(message, locale = 'en-US') {
|
|
22
|
+
const timestamp = getAssistantMessageTimestamp(message)
|
|
23
|
+
if (!timestamp) {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const date = new Date(timestamp)
|
|
28
|
+
if (Number.isNaN(date.getTime())) {
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Intl.DateTimeFormat(locale || 'en-US', {
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit'
|
|
35
|
+
}).format(date)
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
export function listVisibleAssistantMessages(messages = []) {
|
|
6
39
|
return messages.filter((message) => {
|
|
7
40
|
if (message?.role !== 'assistant') {
|
|
@@ -12,6 +45,26 @@ export function listVisibleAssistantMessages(messages = []) {
|
|
|
12
45
|
})
|
|
13
46
|
}
|
|
14
47
|
|
|
48
|
+
export function hasVisibleAssistantHistoryAfter(messages = [], messageId = '') {
|
|
49
|
+
const targetId = String(messageId || '')
|
|
50
|
+
if (!targetId) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const targetIndex = messages.findIndex(message => String(message?.id || '') === targetId)
|
|
55
|
+
if (targetIndex === -1) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return messages.slice(targetIndex + 1).some((message) => {
|
|
60
|
+
if (message?.role === 'assistant') {
|
|
61
|
+
return hasAssistantMessageContent(message)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return hasAssistantMessageContent(message)
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
15
68
|
export const ASSISTANT_MESSAGE_WINDOW_SIZE = 60
|
|
16
69
|
export const ASSISTANT_MESSAGE_WINDOW_STEP = 40
|
|
17
70
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dedupeAssistantSources } from './stream'
|
|
2
|
+
import { getAssistantMessageTimestamp } from './panel'
|
|
2
3
|
|
|
3
4
|
export const ASSISTANT_SESSION_STORAGE_KEY = 'docsector.assistant.session.v1'
|
|
4
5
|
|
|
@@ -17,6 +18,11 @@ function cleanString (value = '', maxLength = Number.MAX_SAFE_INTEGER) {
|
|
|
17
18
|
return String(value || '').slice(0, maxLength)
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function normalizeMessageTimestamp (message) {
|
|
22
|
+
const timestamp = getAssistantMessageTimestamp(message)
|
|
23
|
+
return timestamp ? { timestamp } : {}
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export function normalizeAssistantSession (session = {}) {
|
|
21
27
|
const messages = (Array.isArray(session?.messages) ? session.messages : [])
|
|
22
28
|
.map((message, index) => {
|
|
@@ -24,11 +30,16 @@ export function normalizeAssistantSession (session = {}) {
|
|
|
24
30
|
const content = cleanString(message?.content, MAX_MESSAGE_CONTENT_LENGTH)
|
|
25
31
|
if (!VALID_ROLES.has(role) || !content.trim()) return null
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
const normalizedMessage = {
|
|
28
34
|
id: cleanString(message?.id || `${role}-${index + 1}`, 160),
|
|
29
35
|
role,
|
|
30
36
|
content
|
|
31
37
|
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...normalizedMessage,
|
|
41
|
+
...normalizeMessageTimestamp(message)
|
|
42
|
+
}
|
|
32
43
|
})
|
|
33
44
|
.filter(Boolean)
|
|
34
45
|
.slice(-MAX_PERSISTED_MESSAGES)
|
|
@@ -90,4 +101,4 @@ export function clearAssistantSession ({ storage = null, key = ASSISTANT_SESSION
|
|
|
90
101
|
} catch {
|
|
91
102
|
// Ignore storage failures.
|
|
92
103
|
}
|
|
93
|
-
}
|
|
104
|
+
}
|
|
@@ -8,7 +8,9 @@ import useAssistant from '../composables/useAssistant'
|
|
|
8
8
|
import {
|
|
9
9
|
ASSISTANT_MESSAGE_WINDOW_SIZE,
|
|
10
10
|
ASSISTANT_MESSAGE_WINDOW_STEP,
|
|
11
|
+
formatAssistantMessageTime,
|
|
11
12
|
getAssistantMessageWindow,
|
|
13
|
+
hasVisibleAssistantHistoryAfter,
|
|
12
14
|
isAssistantThinkingState
|
|
13
15
|
} from '../ai-assistant/panel'
|
|
14
16
|
import DPageTokens from './DPageTokens.vue'
|
|
@@ -57,6 +59,8 @@ const scrollArea = ref(null)
|
|
|
57
59
|
const visibleMessageLimit = ref(ASSISTANT_MESSAGE_WINDOW_SIZE)
|
|
58
60
|
const copiedMessageId = ref('')
|
|
59
61
|
const showScrollToBottom = ref(false)
|
|
62
|
+
const retryHistoryDialogOpen = ref(false)
|
|
63
|
+
const pendingRetryMessageId = ref('')
|
|
60
64
|
let scrollFrame = 0
|
|
61
65
|
let copiedMessageTimer = null
|
|
62
66
|
let revealingOlderMessages = false
|
|
@@ -131,6 +135,10 @@ const hasMessageContent = (message) => {
|
|
|
131
135
|
return String(message?.content || '').trim().length > 0
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
const messageTime = (message) => {
|
|
139
|
+
return formatAssistantMessageTime(message, locale.value)
|
|
140
|
+
}
|
|
141
|
+
|
|
134
142
|
const messageHasSources = (message) => {
|
|
135
143
|
return hasSources.value && String(message?.id || '') === latestAssistantMessageId.value
|
|
136
144
|
}
|
|
@@ -281,6 +289,41 @@ const messageCopyIcon = (message) => {
|
|
|
281
289
|
return copiedMessageId.value === String(message?.id || '') ? 'check' : 'content_copy'
|
|
282
290
|
}
|
|
283
291
|
|
|
292
|
+
const runRetryMessage = async (messageId) => {
|
|
293
|
+
const id = String(messageId || '')
|
|
294
|
+
if (!id) return
|
|
295
|
+
await assistant.retryFromUserMessage(id)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const retryMessage = async (message) => {
|
|
299
|
+
const id = String(message?.id || '')
|
|
300
|
+
if (!id || assistant.loading.value) return
|
|
301
|
+
|
|
302
|
+
if (hasVisibleAssistantHistoryAfter(assistant.messages.value, id)) {
|
|
303
|
+
pendingRetryMessageId.value = id
|
|
304
|
+
retryHistoryDialogOpen.value = true
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await runRetryMessage(id)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const clearPendingRetryMessage = () => {
|
|
312
|
+
pendingRetryMessageId.value = ''
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const cancelRetryMessage = () => {
|
|
316
|
+
retryHistoryDialogOpen.value = false
|
|
317
|
+
clearPendingRetryMessage()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const confirmRetryMessage = async () => {
|
|
321
|
+
const id = pendingRetryMessageId.value
|
|
322
|
+
retryHistoryDialogOpen.value = false
|
|
323
|
+
clearPendingRetryMessage()
|
|
324
|
+
await runRetryMessage(id)
|
|
325
|
+
}
|
|
326
|
+
|
|
284
327
|
const submit = async (value = input.value) => {
|
|
285
328
|
const prompt = String(value || '').trim()
|
|
286
329
|
if (!prompt) return
|
|
@@ -419,7 +462,7 @@ onBeforeUnmount(() => {
|
|
|
419
462
|
<q-btn
|
|
420
463
|
flat dense
|
|
421
464
|
text-color="primary"
|
|
422
|
-
class="d-assistant-message__copy d-assistant-message__copy--assistant"
|
|
465
|
+
class="d-assistant-message__action d-assistant-message__copy d-assistant-message__copy--assistant"
|
|
423
466
|
:icon="messageCopyIcon(message)"
|
|
424
467
|
:aria-label="t('assistant.copyMessage')"
|
|
425
468
|
@click="copyMessage(message)"
|
|
@@ -483,6 +526,11 @@ onBeforeUnmount(() => {
|
|
|
483
526
|
</q-list>
|
|
484
527
|
</q-menu>
|
|
485
528
|
</q-chip>
|
|
529
|
+
|
|
530
|
+
<span
|
|
531
|
+
v-if="messageTime(message)"
|
|
532
|
+
class="d-assistant-message__timestamp"
|
|
533
|
+
>{{ messageTime(message) }}</span>
|
|
486
534
|
</div>
|
|
487
535
|
|
|
488
536
|
<div
|
|
@@ -491,15 +539,30 @@ onBeforeUnmount(() => {
|
|
|
491
539
|
>
|
|
492
540
|
<div class="d-assistant-message__hoverlayer">
|
|
493
541
|
<q-btn
|
|
542
|
+
v-if="!assistant.loading.value"
|
|
494
543
|
flat dense
|
|
495
544
|
text-color="primary"
|
|
496
|
-
class="d-assistant-
|
|
545
|
+
class="d-assistant-message__action d-assistant-message__retry"
|
|
546
|
+
icon="refresh"
|
|
547
|
+
:aria-label="t('assistant.retryMessage')"
|
|
548
|
+
@click="retryMessage(message)"
|
|
549
|
+
>
|
|
550
|
+
<q-tooltip>{{ t('assistant.retryMessage') }}</q-tooltip>
|
|
551
|
+
</q-btn>
|
|
552
|
+
<q-btn
|
|
553
|
+
flat dense
|
|
554
|
+
text-color="primary"
|
|
555
|
+
class="d-assistant-message__action d-assistant-message__copy d-assistant-message__copy--user"
|
|
497
556
|
:icon="messageCopyIcon(message)"
|
|
498
557
|
:aria-label="t('assistant.copyMessage')"
|
|
499
558
|
@click="copyMessage(message)"
|
|
500
559
|
>
|
|
501
560
|
<q-tooltip>{{ t('assistant.copyMessage') }}</q-tooltip>
|
|
502
561
|
</q-btn>
|
|
562
|
+
<span
|
|
563
|
+
v-if="messageTime(message)"
|
|
564
|
+
class="d-assistant-message__timestamp"
|
|
565
|
+
>{{ messageTime(message) }}</span>
|
|
503
566
|
</div>
|
|
504
567
|
</div>
|
|
505
568
|
</div>
|
|
@@ -572,6 +635,33 @@ onBeforeUnmount(() => {
|
|
|
572
635
|
</div>
|
|
573
636
|
</div>
|
|
574
637
|
</footer>
|
|
638
|
+
|
|
639
|
+
<q-dialog v-model="retryHistoryDialogOpen" @hide="clearPendingRetryMessage">
|
|
640
|
+
<q-card class="d-assistant-retry-dialog">
|
|
641
|
+
<q-card-section class="d-assistant-retry-dialog__header">
|
|
642
|
+
<q-icon name="warning_amber" size="24px" />
|
|
643
|
+
<div>
|
|
644
|
+
<h3>{{ t('assistant.retryHistoryTitle') }}</h3>
|
|
645
|
+
<p>{{ t('assistant.retryHistoryMessage') }}</p>
|
|
646
|
+
</div>
|
|
647
|
+
</q-card-section>
|
|
648
|
+
<q-card-actions align="right" class="d-assistant-retry-dialog__actions">
|
|
649
|
+
<q-btn
|
|
650
|
+
unelevated no-caps
|
|
651
|
+
color="grey-7"
|
|
652
|
+
text-color="white"
|
|
653
|
+
:label="t('assistant.retryHistoryCancel')"
|
|
654
|
+
@click="cancelRetryMessage"
|
|
655
|
+
/>
|
|
656
|
+
<q-btn
|
|
657
|
+
unelevated no-caps
|
|
658
|
+
color="primary"
|
|
659
|
+
:label="t('assistant.retryHistoryConfirm')"
|
|
660
|
+
@click="confirmRetryMessage"
|
|
661
|
+
/>
|
|
662
|
+
</q-card-actions>
|
|
663
|
+
</q-card>
|
|
664
|
+
</q-dialog>
|
|
575
665
|
</aside>
|
|
576
666
|
</template>
|
|
577
667
|
|
|
@@ -636,7 +726,7 @@ onBeforeUnmount(() => {
|
|
|
636
726
|
color: rgba(255, 255, 255, 0.86)
|
|
637
727
|
border-color: rgba(255, 255, 255, 0.12)
|
|
638
728
|
|
|
639
|
-
.d-assistant-
|
|
729
|
+
.d-assistant-message__action
|
|
640
730
|
color: rgba(255, 255, 255, 0.84)
|
|
641
731
|
|
|
642
732
|
.d-assistant-sources-chip__avatar
|
|
@@ -862,10 +952,12 @@ onBeforeUnmount(() => {
|
|
|
862
952
|
color: white
|
|
863
953
|
background: var(--q-primary)
|
|
864
954
|
|
|
865
|
-
&:hover .d-assistant-message__hoverlayer .d-assistant-
|
|
866
|
-
&:focus-within .d-assistant-message__hoverlayer .d-assistant-
|
|
867
|
-
.d-assistant-message__hoverlayer:hover .d-assistant-
|
|
868
|
-
.d-assistant-message__hoverlayer:focus-within .d-assistant-
|
|
955
|
+
&:hover .d-assistant-message__hoverlayer .d-assistant-message__action,
|
|
956
|
+
&:focus-within .d-assistant-message__hoverlayer .d-assistant-message__action,
|
|
957
|
+
.d-assistant-message__hoverlayer:hover .d-assistant-message__action,
|
|
958
|
+
.d-assistant-message__hoverlayer:focus-within .d-assistant-message__action,
|
|
959
|
+
&:hover .d-assistant-message__timestamp,
|
|
960
|
+
&:focus-within .d-assistant-message__timestamp
|
|
869
961
|
opacity: 1
|
|
870
962
|
pointer-events: auto
|
|
871
963
|
|
|
@@ -881,6 +973,10 @@ onBeforeUnmount(() => {
|
|
|
881
973
|
.d-assistant-message__footer
|
|
882
974
|
justify-content: flex-start
|
|
883
975
|
|
|
976
|
+
&:hover .d-assistant-message__timestamp,
|
|
977
|
+
&:focus-within .d-assistant-message__timestamp
|
|
978
|
+
opacity: 1
|
|
979
|
+
|
|
884
980
|
&__content
|
|
885
981
|
max-width: 88%
|
|
886
982
|
min-width: 0
|
|
@@ -935,7 +1031,7 @@ onBeforeUnmount(() => {
|
|
|
935
1031
|
|
|
936
1032
|
.d-assistant-sources-chip
|
|
937
1033
|
flex: 0 1 auto
|
|
938
|
-
max-width: calc(100% -
|
|
1034
|
+
max-width: calc(100% - 92px)
|
|
939
1035
|
|
|
940
1036
|
&--user
|
|
941
1037
|
width: auto
|
|
@@ -946,7 +1042,7 @@ onBeforeUnmount(() => {
|
|
|
946
1042
|
align-items: center
|
|
947
1043
|
justify-content: center
|
|
948
1044
|
|
|
949
|
-
&
|
|
1045
|
+
&__action
|
|
950
1046
|
flex: 0 0 auto
|
|
951
1047
|
width: 30px
|
|
952
1048
|
height: 30px
|
|
@@ -960,25 +1056,45 @@ onBeforeUnmount(() => {
|
|
|
960
1056
|
|
|
961
1057
|
i
|
|
962
1058
|
font-size: 17px !important
|
|
963
|
-
margin
|
|
1059
|
+
margin: 0
|
|
964
1060
|
|
|
1061
|
+
&__copy
|
|
965
1062
|
&--assistant
|
|
966
1063
|
margin-left: -2px
|
|
967
1064
|
|
|
1065
|
+
&__retry
|
|
1066
|
+
color: currentColor
|
|
1067
|
+
|
|
968
1068
|
&__hoverlayer
|
|
969
1069
|
display: flex
|
|
970
1070
|
align-items: center
|
|
971
|
-
justify-content:
|
|
972
|
-
|
|
1071
|
+
justify-content: flex-end
|
|
1072
|
+
gap: 6px
|
|
1073
|
+
width: auto
|
|
973
1074
|
height: 30px
|
|
974
1075
|
min-width: 30px
|
|
975
1076
|
min-height: 30px
|
|
976
1077
|
|
|
977
|
-
.d-assistant-
|
|
1078
|
+
.d-assistant-message__action
|
|
978
1079
|
opacity: 0
|
|
979
1080
|
pointer-events: none
|
|
980
1081
|
transition: opacity 0.14s ease
|
|
981
1082
|
|
|
1083
|
+
&__timestamp
|
|
1084
|
+
flex: 0 0 auto
|
|
1085
|
+
margin-left: auto
|
|
1086
|
+
color: currentColor
|
|
1087
|
+
font-size: 0.72rem
|
|
1088
|
+
font-weight: 700
|
|
1089
|
+
font-variant-numeric: tabular-nums
|
|
1090
|
+
line-height: 30px
|
|
1091
|
+
opacity: 0
|
|
1092
|
+
pointer-events: none
|
|
1093
|
+
transition: opacity 0.14s ease
|
|
1094
|
+
|
|
1095
|
+
&__hoverlayer &__timestamp
|
|
1096
|
+
margin-left: 0
|
|
1097
|
+
|
|
982
1098
|
&__thinking
|
|
983
1099
|
display: flex
|
|
984
1100
|
align-items: center
|
|
@@ -987,6 +1103,31 @@ onBeforeUnmount(() => {
|
|
|
987
1103
|
opacity: 0.78
|
|
988
1104
|
font-weight: 600
|
|
989
1105
|
|
|
1106
|
+
.d-assistant-retry-dialog
|
|
1107
|
+
width: min(360px, calc(100vw - 32px))
|
|
1108
|
+
border-radius: 8px
|
|
1109
|
+
|
|
1110
|
+
&__header
|
|
1111
|
+
display: flex
|
|
1112
|
+
align-items: flex-start
|
|
1113
|
+
gap: 12px
|
|
1114
|
+
padding: 18px 18px 10px
|
|
1115
|
+
|
|
1116
|
+
h3
|
|
1117
|
+
margin: 0 0 6px
|
|
1118
|
+
font-size: 1rem
|
|
1119
|
+
line-height: 1.3
|
|
1120
|
+
font-weight: 800
|
|
1121
|
+
|
|
1122
|
+
p
|
|
1123
|
+
margin: 0
|
|
1124
|
+
color: currentColor
|
|
1125
|
+
opacity: 0.72
|
|
1126
|
+
line-height: 1.45
|
|
1127
|
+
|
|
1128
|
+
&__actions
|
|
1129
|
+
padding: 8px 12px 14px
|
|
1130
|
+
|
|
990
1131
|
.d-assistant-sources-chip
|
|
991
1132
|
max-width: 100%
|
|
992
1133
|
min-width: 0
|
|
@@ -17,10 +17,13 @@ let assistantSessionPersistencePaused = false
|
|
|
17
17
|
const ASSISTANT_SESSION_PERSIST_DEBOUNCE = 180
|
|
18
18
|
|
|
19
19
|
function createMessage (role, content = '') {
|
|
20
|
+
const timestamp = Date.now()
|
|
21
|
+
|
|
20
22
|
return {
|
|
21
|
-
id: `${role}-${
|
|
23
|
+
id: `${role}-${timestamp}-${Math.random().toString(16).slice(2)}`,
|
|
22
24
|
role,
|
|
23
|
-
content
|
|
25
|
+
content,
|
|
26
|
+
timestamp
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -121,6 +124,11 @@ export default function useAssistant ({ route, locale, getContext } = {}) {
|
|
|
121
124
|
message.content += content
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
const appendAssistantPlaceholder = () => {
|
|
128
|
+
messages.value.push(createMessage('assistant'))
|
|
129
|
+
return messages.value[messages.value.length - 1]
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
const consumeStream = async (response, assistantMessage) => {
|
|
125
133
|
const reader = response.body.getReader()
|
|
126
134
|
const decoder = new TextDecoder()
|
|
@@ -175,22 +183,14 @@ export default function useAssistant ({ route, locale, getContext } = {}) {
|
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
const
|
|
179
|
-
const prompt = String(content || '').trim()
|
|
180
|
-
if (!prompt || loading.value) return
|
|
181
|
-
|
|
186
|
+
const prepareRequest = () => {
|
|
182
187
|
assistantSessionPersistencePaused = true
|
|
183
188
|
cancelPersistAssistantSession()
|
|
184
189
|
error.value = ''
|
|
185
190
|
sources.value = []
|
|
191
|
+
}
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
const assistantMessage = createMessage('assistant')
|
|
189
|
-
messages.value.push(userMessage, assistantMessage)
|
|
190
|
-
// Use the reactive proxy from the array so streamed mutations trigger
|
|
191
|
-
// live re-renders (raw object mutations bypass Vue reactivity).
|
|
192
|
-
const liveAssistantMessage = messages.value[messages.value.length - 1]
|
|
193
|
-
|
|
193
|
+
const requestAssistantResponse = async (liveAssistantMessage) => {
|
|
194
194
|
abortController.value = new AbortController()
|
|
195
195
|
loading.value = true
|
|
196
196
|
|
|
@@ -249,6 +249,36 @@ export default function useAssistant ({ route, locale, getContext } = {}) {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
const send = async (content) => {
|
|
253
|
+
const prompt = String(content || '').trim()
|
|
254
|
+
if (!prompt || loading.value) return
|
|
255
|
+
|
|
256
|
+
prepareRequest()
|
|
257
|
+
|
|
258
|
+
messages.value.push(createMessage('user', prompt))
|
|
259
|
+
const liveAssistantMessage = appendAssistantPlaceholder()
|
|
260
|
+
|
|
261
|
+
await requestAssistantResponse(liveAssistantMessage)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const retryFromUserMessage = async (messageId) => {
|
|
265
|
+
const targetId = String(messageId || '')
|
|
266
|
+
if (!targetId || loading.value) return
|
|
267
|
+
|
|
268
|
+
const targetIndex = messages.value.findIndex(message => String(message?.id || '') === targetId)
|
|
269
|
+
const targetMessage = messages.value[targetIndex]
|
|
270
|
+
if (targetIndex === -1 || targetMessage?.role !== 'user' || !String(targetMessage?.content || '').trim()) {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
prepareRequest()
|
|
275
|
+
|
|
276
|
+
messages.value = messages.value.slice(0, targetIndex + 1)
|
|
277
|
+
const liveAssistantMessage = appendAssistantPlaceholder()
|
|
278
|
+
|
|
279
|
+
await requestAssistantResponse(liveAssistantMessage)
|
|
280
|
+
}
|
|
281
|
+
|
|
252
282
|
return {
|
|
253
283
|
config: assistantConfig,
|
|
254
284
|
messages,
|
|
@@ -257,6 +287,7 @@ export default function useAssistant ({ route, locale, getContext } = {}) {
|
|
|
257
287
|
error,
|
|
258
288
|
hasMessages,
|
|
259
289
|
send,
|
|
290
|
+
retryFromUserMessage,
|
|
260
291
|
stop,
|
|
261
292
|
clear
|
|
262
293
|
}
|
package/src/i18n/helpers.js
CHANGED
|
@@ -84,6 +84,11 @@ const engineDefaults = {
|
|
|
84
84
|
sources: 'Sources',
|
|
85
85
|
sourcesCount: '{count} sources',
|
|
86
86
|
copyMessage: 'Copy message',
|
|
87
|
+
retryMessage: 'Reload message',
|
|
88
|
+
retryHistoryTitle: 'Reload from this message?',
|
|
89
|
+
retryHistoryMessage: 'Messages after this question will be removed from the conversation.',
|
|
90
|
+
retryHistoryConfirm: 'Reload',
|
|
91
|
+
retryHistoryCancel: 'Cancel',
|
|
87
92
|
copied: 'Message copied',
|
|
88
93
|
loadEarlier: 'Load earlier messages',
|
|
89
94
|
thinking: 'Searching the docs…',
|
|
@@ -157,6 +162,11 @@ const engineDefaults = {
|
|
|
157
162
|
sources: 'Fontes',
|
|
158
163
|
sourcesCount: '{count} fontes',
|
|
159
164
|
copyMessage: 'Copiar mensagem',
|
|
165
|
+
retryMessage: 'Reenviar mensagem',
|
|
166
|
+
retryHistoryTitle: 'Reenviar desta mensagem?',
|
|
167
|
+
retryHistoryMessage: 'As mensagens depois desta pergunta serão removidas da conversa.',
|
|
168
|
+
retryHistoryConfirm: 'Reenviar',
|
|
169
|
+
retryHistoryCancel: 'Cancelar',
|
|
160
170
|
copied: 'Mensagem copiada',
|
|
161
171
|
loadEarlier: 'Carregar mensagens anteriores',
|
|
162
172
|
thinking: 'Consultando a documentação…',
|
|
@@ -119,6 +119,11 @@
|
|
|
119
119
|
sources: 'Sources',
|
|
120
120
|
sourcesCount: '{count} sources',
|
|
121
121
|
copyMessage: 'Copy message',
|
|
122
|
+
retryMessage: 'Reload message',
|
|
123
|
+
retryHistoryTitle: 'Reload from this message?',
|
|
124
|
+
retryHistoryMessage: 'Messages after this question will be removed from the conversation.',
|
|
125
|
+
retryHistoryConfirm: 'Reload',
|
|
126
|
+
retryHistoryCancel: 'Cancel',
|
|
122
127
|
copied: 'Message copied',
|
|
123
128
|
loadEarlier: 'Load earlier messages',
|
|
124
129
|
thinking: 'Searching the docs…',
|
|
@@ -118,6 +118,11 @@
|
|
|
118
118
|
sources: 'Fontes',
|
|
119
119
|
sourcesCount: '{count} fontes',
|
|
120
120
|
copyMessage: 'Copiar mensagem',
|
|
121
|
+
retryMessage: 'Reenviar mensagem',
|
|
122
|
+
retryHistoryTitle: 'Reenviar desta mensagem?',
|
|
123
|
+
retryHistoryMessage: 'As mensagens depois desta pergunta serão removidas da conversa.',
|
|
124
|
+
retryHistoryConfirm: 'Reenviar',
|
|
125
|
+
retryHistoryCancel: 'Cancelar',
|
|
121
126
|
copied: 'Mensagem copiada',
|
|
122
127
|
loadEarlier: 'Carregar mensagens anteriores',
|
|
123
128
|
thinking: 'Consultando a documentação…',
|