@docsector/docsector-reader 4.5.5 β 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 +7 -11
- package/bin/docsector.js +5 -5
- 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
|
@@ -34,6 +34,7 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
34
34
|
- πͺͺ **MCP Server Card** β Optional `/.well-known/mcp/server-card.json` for MCP server discovery before connection
|
|
35
35
|
- π **WebMCP Browser Tools** β Optional registration of in-page tools via `navigator.modelContext` for browser agents
|
|
36
36
|
- π€ **AI Assistant Panel** β Optional documentation assistant drawer backed by Cloudflare AI Search through an internal same-origin endpoint
|
|
37
|
+
- ποΈ **API Catalog Well-Known** β Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
|
|
37
38
|
- π **Homepage Link Headers** β Auto-generated `Link` response headers for agent discovery (`api-catalog`, `service-doc`, `service-desc`, `describedby`) per RFC 8288 / RFC 9727
|
|
38
39
|
- π **MCP Server** β Auto-generated [MCP](https://modelcontextprotocol.io) server at `/mcp` for AI assistant integration (Claude Desktop, VS Code, etc.)
|
|
39
40
|
- π **llms.txt / llms-full.txt** β Auto-generated [llms.txt](https://llmstxt.org) index and full-content file for LLM discovery (requires `siteUrl` in config)
|
|
@@ -58,7 +59,7 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
58
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
|
|
59
60
|
- π±οΈ **Active Menu Item UX** β Active menu entries keep pointer cursor, clear URL hash without redundant navigation, and prevent accidental label text selection
|
|
60
61
|
- π **Search** β Menu search across all documentation content and tags
|
|
61
|
-
- π¬ **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
|
|
62
63
|
- π± **Responsive** β Mobile-friendly with collapsible sidebar and drawers
|
|
63
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
|
|
64
65
|
- π **Book Tabs with Per-State Colors** β Define `*.book.js` tabs with icons, order, and `color.active` / `color.inactive`
|
|
@@ -77,9 +78,6 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
77
78
|
- π§± **Configurable Homepage Layout** β Set `homePage.layout` to `default` or `fullwidth`; fullwidth keeps the header and book tabs while removing the sidebar, subpage toolbar, Table of Contents, and homepage footer
|
|
78
79
|
- π **Remote README as Home** β Optional build-time remote README source for homepage with automatic local fallback and automatic primary-title handoff when the remote README already provides the project heading
|
|
79
80
|
- π **GitHub-Compatible Heading Anchors** β Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
|
|
80
|
-
- 𧬠**Scaffolded Homepage Override Wiring** β New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
81
|
-
- π€ **Scaffolded AI Assistant Config** β New consumer projects include a ready-to-enable `aiAssistant` example in `docsector.config.js` so the built-in assistant is visible in the default scaffold
|
|
82
|
-
- π§° **Scaffolded Dev Reliability** β New consumer projects protect Docsector virtual registries and Markdown CommonJS plugins from Vite optimizer edge cases during dev and build
|
|
83
81
|
- π **Expandable Markdown Sections** β Use `<d-block-expandable title="...">...</d-block-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
|
|
84
82
|
- 1οΈβ£ **Stepper Guides** β Use `<d-block-stepper>` with nested `<d-block-step title="...">...</d-block-step>` items to render native Quasar vertical steppers with rich Markdown and optional per-step icon overrides
|
|
85
83
|
- π **Timeline Updates** β Use `<d-block-timeline>` with nested `<d-block-timeline-item date="...">...</d-block-timeline-item>` entries and optional `<d-block-timeline-tag>` labels to publish GitBook-inspired changelog items with direct-link anchors, tag icons/colors, and rich Markdown bodies
|
|
@@ -88,13 +86,11 @@ Transform Markdown content into beautiful, navigable documentation sites β wit
|
|
|
88
86
|
- π§ **Quick Links Custom Element** β Use `<d-block-quick-links>` and `<d-block-quick-link>` in Markdown to render rich home navigation cards
|
|
89
87
|
- ποΈ **Cards Custom Element** β Use `<d-block-cards>` and `<d-block-card>` in Markdown to render linked content cards with optional cover images
|
|
90
88
|
- π§Ύ **API JSON Reference Block** β Use `<d-block-api src="..." />` in Markdown to render Quasar-compatible API reference UIs from public JSON assets without inventing a new schema
|
|
91
|
-
- ποΈ **API Catalog Well-Known** β Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
|
|
92
89
|
- π§ **Docsector Authoring Skill Docs** β Documents the built-in `SKILL.md` and reference files so agents can learn Docsector blocks, page patterns, MCP lookup, and WebMCP tools from a public manual page
|
|
93
90
|
- ποΈ **Multi-Version History** β Archive older major versions under `src/pages/.old/<version>/` and expose them at prefixed routes (e.g. `/v0.x/guide/...`) while keeping the current docs at unprefixed routes
|
|
94
91
|
- π·οΈ **Version Selector Badges** β Every version in the sidebar selector displays a color-coded badge: green for released, orange for draft, red for deprecated; fully customizable via `badge: { label, color, textColor }`
|
|
95
92
|
- π **Tabbed Code Blocks** β Group consecutive fenced code blocks into tabs using the `group` and `tab` attributes in the fence info line
|
|
96
93
|
- π§ͺ **Live Code Example Blocks** β Use `<d-block-code-example src="..." />` to render bundled Vue SFC examples with a live preview, GitHub source link, source toggle, and CodePen export for compatible examples
|
|
97
|
-
- π **Accurate Source Code Line Counts** β Code example headers count visible lines correctly across LF, CRLF, and terminal newlines without inflating the total
|
|
98
94
|
- π **Breadcrumb Path Display** β Show a file path breadcrumb above code blocks with the `breadcrumb` attribute; renders as clickable path segments
|
|
99
95
|
- π¨ **File Type Icons** β Automatically resolves file extension or filename to a Material Icon Theme SVG icon, shown inline in tabs and beside the last breadcrumb segment
|
|
100
96
|
- βοΈ **Single Config File** β Customize branding, links, and languages via `docsector.config.js`
|
|
@@ -305,7 +301,7 @@ Check `checks.discovery.webMcp.status` equals `"pass"`.
|
|
|
305
301
|
|
|
306
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.
|
|
307
303
|
|
|
308
|
-
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.
|
|
309
305
|
|
|
310
306
|
### Configure
|
|
311
307
|
|
|
@@ -336,8 +332,8 @@ export default {
|
|
|
336
332
|
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
337
333
|
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
338
334
|
model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
339
|
-
retrievalType: '
|
|
340
|
-
maxResults:
|
|
335
|
+
retrievalType: 'vector',
|
|
336
|
+
maxResults: 10,
|
|
341
337
|
matchThreshold: 0.4,
|
|
342
338
|
contextExpansion: 1,
|
|
343
339
|
queryRewrite: { enabled: true },
|
|
@@ -890,8 +886,8 @@ export default {
|
|
|
890
886
|
instanceNameEnv: 'AI_SEARCH_INSTANCE_NAME',
|
|
891
887
|
accountIdEnv: 'CLOUDFLARE_ACCOUNT_ID',
|
|
892
888
|
apiTokenEnv: 'CLOUDFLARE_API_TOKEN',
|
|
893
|
-
retrievalType: '
|
|
894
|
-
maxResults:
|
|
889
|
+
retrievalType: 'vector',
|
|
890
|
+
maxResults: 10,
|
|
895
891
|
stream: true
|
|
896
892
|
}
|
|
897
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`
|
|
@@ -553,15 +553,15 @@ const TEMPLATE_HOMEPAGE_MD = `\
|
|
|
553
553
|
|
|
554
554
|
Docsector Reader is a markdown-first documentation engine.
|
|
555
555
|
|
|
556
|
+
Give us a star on GitHub: [docsector/docsector-reader](https://github.com/docsector/docsector-reader)
|
|
557
|
+
|
|
556
558
|
## Quick Links
|
|
557
559
|
|
|
558
560
|
- [Getting Started](/guide/getting-started/overview/)
|
|
559
|
-
- [Configuration](/guide/configuration/overview/)
|
|
560
|
-
- [Pages and Routing](/guide/pages-and-routing/overview/)
|
|
561
561
|
|
|
562
|
-
##
|
|
562
|
+
## Demo
|
|
563
563
|
|
|
564
|
-
|
|
564
|
+
Check out the [docsector.com](https://docsector.com) to see all the features in action!
|
|
565
565
|
`
|
|
566
566
|
|
|
567
567
|
const TEMPLATE_404_PAGE = `\
|
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β¦',
|