@meistrari/chat-nuxt 4.0.1 → 4.1.0-rc.1
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 +3 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -1
- package/dist/runtime/assets/css/markstream.css +1 -1
- package/dist/runtime/embed/components/chat-embed-inner.vue +18 -5
- package/dist/runtime/messages/components/chat-mention-dropdown.d.vue.ts +6 -0
- package/dist/runtime/messages/components/chat-mention-dropdown.vue +37 -5
- package/dist/runtime/messages/components/chat-mention-dropdown.vue.d.ts +6 -0
- package/dist/runtime/messages/components/chat-message-bubble.d.vue.ts +2 -0
- package/dist/runtime/messages/components/chat-message-bubble.vue +23 -25
- package/dist/runtime/messages/components/chat-message-bubble.vue.d.ts +2 -0
- package/dist/runtime/messages/components/chat-message-input.vue +4 -0
- package/dist/runtime/messages/components/chat-message-list.vue +2 -0
- package/dist/runtime/messages/composables/mention-autocomplete.d.ts +2 -0
- package/dist/runtime/messages/composables/mention-autocomplete.js +69 -19
- package/dist/runtime/messages/debug/chat-debug-scenarios.js +9 -1
- package/dist/runtime/messages/user-citations.d.ts +11 -0
- package/dist/runtime/messages/user-citations.js +31 -0
- package/dist/runtime/messages/user-mentions.d.ts +8 -0
- package/dist/runtime/messages/user-mentions.js +20 -0
- package/dist/runtime/types/composer.d.ts +13 -0
- package/dist/runtime/types/messages.d.ts +21 -0
- package/dist/runtime/types/messages.js +2 -0
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -28,7 +28,8 @@ Runtime internals under `src/runtime/**`, generated Nuxt aliases, and workspace-
|
|
|
28
28
|
| [Citations](docs/guides/citations.md) | `cite://` chips, metadata popover, document panel |
|
|
29
29
|
| [Feature flags](docs/guides/feature-flags.md) | Usage tab, debug entry, cancel button, PostHog wiring |
|
|
30
30
|
| [Message feedback](docs/guides/message-feedback.md) | 👍/👎 controls and host-owned persistence |
|
|
31
|
-
| [Composer extension](docs/guides/composer-extension.md) | Host actions, shared attachment strip, outbound content and retry lifecycle |
|
|
31
|
+
| [Composer extension](docs/guides/composer-extension.md) | Host actions, shared attachment strip, outbound content and retry lifecycle, "/" commands |
|
|
32
|
+
| [Interactive message widgets](docs/guides/message-widgets.md) | Message context and programmatic send for `customComponents` widgets |
|
|
32
33
|
| [Workspace settings](docs/guides/workspace-settings.md) | Settings overrides, agent runtime ownership, `<ChatConfigurationModal>` |
|
|
33
34
|
| [Upgrading to 4.0](docs/guides/upgrading-to-4.md) | Required Chat API version for v2 sends, upgrade order, vue-i18n removal |
|
|
34
35
|
|
|
@@ -71,7 +72,7 @@ Declared in `src/runtime/embed/types.ts`; prop, feature-flag, and event payload
|
|
|
71
72
|
| `labels` | `Partial<ChatLabels>` | Portuguese defaults | Override conversation-search text; see [UI labels](docs/guides/embedding.md#ui-labels) for supported keys and surfaces |
|
|
72
73
|
| `customComponents` | `Record<string, Component>` | `undefined` | Custom markdown renderers keyed by markdown node or tag name. Components receive `node` plus the parser lifecycle props `loading` and `autoClosed` |
|
|
73
74
|
| `customHtmlTags` | `readonly string[]` | `undefined` | Extra HTML tags allowed by the markdown renderer |
|
|
74
|
-
| `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant messages (see [Citations](docs/guides/citations.md)) |
|
|
75
|
+
| `citations` | `ChatCitationsConfig \| null` | `undefined` | Enables `cite://` citations in assistant and user messages (see [Citations](docs/guides/citations.md)) |
|
|
75
76
|
| `feedbackConfig` | `MessageFeedbackConfig \| null` | `undefined` | Enables 👍/👎 feedback on assistant messages; providing the prop (even `{}`) shows the controls |
|
|
76
77
|
| `messageFeedback` | `Record<string, MessageFeedbackRating> \| null` | `undefined` | Host-persisted votes keyed by message id; when provided, it is the single source of truth for the selected thumbs |
|
|
77
78
|
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -190,7 +190,9 @@ const FULL_NAME_COMPONENT_DIRS = ["citations", "conversations", "embed", "files"
|
|
|
190
190
|
const PATH_PREFIX_COMPONENT_DIRS = ["rendering", "shared"];
|
|
191
191
|
const DYNAMIC_ICON_SAFELIST = [
|
|
192
192
|
"i-ph-folder-minus",
|
|
193
|
-
"i-ph-folder-open"
|
|
193
|
+
"i-ph-folder-open",
|
|
194
|
+
// Neutral pill icon for host-defined user mentions (user-mentions.ts).
|
|
195
|
+
"i-ph-paperclip"
|
|
194
196
|
];
|
|
195
197
|
function resolveChatAuthMode(runtimeConfig) {
|
|
196
198
|
const publicConfig = runtimeConfig.public;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.markstream-vue{font-family:inherit;--at-apply:"body-16-regular leading-24px text-primary";--list-item-marker:#a3a3a3;--list-item-counter-marker:#262626}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;letter-spacing:0;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{--at-apply:"font-580"}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ol.list-node.list-node{padding-left:3em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{--at-apply:"body-16-regular leading-24px";margin:0}.markstream-vue>.node-slot[data-node-type=paragraph]:not(:first-child){margin-top:12px}@keyframes chat-stream-chunk-in{0%{opacity:0}to{opacity:1}}.markstream-vue .chat-stream-chunk{animation:chat-stream-chunk-in var(--chat-stream-fade-duration,.32s) cubic-bezier(.2,0,0,1) both}@media (prefers-reduced-motion:reduce){.markstream-vue .chat-stream-chunk{animation:none}}.markstream-vue .typewriter-cursor,.markstream-vue .typewriter-simple-cursor .typewriter-simple-cursor-target:after{display:none}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{--at-apply:"heading-h2-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{--at-apply:"heading-h3-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{--at-apply:"heading-h4-semibold text-primary";margin:20px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue .heading-node.heading-5,.markstream-vue .heading-node.heading-6,.markstream-vue h4,.markstream-vue h5,.markstream-vue h6{--at-apply:"heading-h5-semibold text-primary";margin:16px 0 8px}.markstream-vue>.node-slot:first-child .heading-node{margin-top:0}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag>span[class*=i-ph]{flex-shrink:0}
|
|
1
|
+
.markstream-vue{font-family:inherit;--at-apply:"body-16-regular leading-24px text-primary";--list-item-marker:#a3a3a3;--list-item-counter-marker:#262626}.markstream-vue code,.markstream-vue kbd,.markstream-vue pre,.markstream-vue samp{font-family:Geist Mono Variable,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:13px;font-variation-settings:normal;letter-spacing:0;line-height:1.5}.markstream-vue ol,.markstream-vue ul{margin:8px 0;padding-left:1.5em}.markstream-vue .hr-node{border-color:#e5e5e5;border-top-width:.5px;margin-bottom:24px;margin-top:24px}.markstream-vue .strong-node{--at-apply:"font-580"}.markstream-vue .list-node.list-node{margin-bottom:8px;margin-left:4px;margin-top:8px;padding-left:1em}.markstream-vue ol.list-node.list-node{padding-left:3em}.markstream-vue ul{list-style:disc;list-style-position:outside}.markstream-vue ol{list-style:decimal;list-style-position:outside}.markstream-vue li{margin:4px 0}.markstream-vue .paragraph-node{--at-apply:"body-16-regular leading-24px";margin:0}.markstream-vue>.node-slot[data-node-type=paragraph]:not(:first-child){margin-top:12px}@keyframes chat-stream-chunk-in{0%{opacity:0}to{opacity:1}}.markstream-vue .chat-stream-chunk{animation:chat-stream-chunk-in var(--chat-stream-fade-duration,.32s) cubic-bezier(.2,0,0,1) both}@media (prefers-reduced-motion:reduce){.markstream-vue .chat-stream-chunk{animation:none}}.markstream-vue .typewriter-cursor,.markstream-vue .typewriter-simple-cursor .typewriter-simple-cursor-target:after{display:none}.markstream-vue .my-8{margin-bottom:8px;margin-top:8px}.markstream-vue .heading-node.heading-1,.markstream-vue h1{--at-apply:"heading-h2-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-2,.markstream-vue h2{--at-apply:"heading-h3-semibold text-primary";margin:24px 0 8px}.markstream-vue .heading-node.heading-3,.markstream-vue h3{--at-apply:"heading-h4-semibold text-primary";margin:20px 0 8px}.markstream-vue .heading-node.heading-4,.markstream-vue .heading-node.heading-5,.markstream-vue .heading-node.heading-6,.markstream-vue h4,.markstream-vue h5,.markstream-vue h6{--at-apply:"heading-h5-semibold text-primary";margin:16px 0 8px}.markstream-vue>.node-slot:first-child .heading-node{margin-top:0}.markstream-vue [class*=tooltip],[class*=tooltip-element]{background-color:var(--neutral-900,#1a1a1a)!important;border-radius:4px;color:#fff!important;font-size:12px;padding:4px 8px}.mention-tag{align-items:center;border-radius:9999px;display:inline-flex;font-size:.875em;font-weight:500;gap:4px;padding:2px 8px;vertical-align:middle;white-space:nowrap}.mention-tag-file{background-color:rgba(127,189,247,.12);color:#7fbdf7}.mention-tag-skill{background-color:rgba(155,111,222,.12);color:#9b6fde}.mention-tag-canvas{background-color:rgba(234,161,65,.12);color:#d4882e}.mention-tag-generic{background-color:rgba(148,163,184,.14);color:#64748b}.mention-tag>span[class*=i-ph]{flex-shrink:0}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { inject } from "vue";
|
|
2
|
+
import { inject, provide } from "vue";
|
|
3
3
|
import { CHAT_COMPOSER_EXTENSION_KEY } from "../../types/composer";
|
|
4
|
+
import { CHAT_MESSAGE_SEND_KEY } from "../../types/messages";
|
|
4
5
|
import { useChatAuth } from "#chat-auth";
|
|
5
6
|
import { useBreakpoints } from "@vueuse/core";
|
|
6
7
|
import { useCitationPanel } from "../../citations/composables/citation-panel";
|
|
@@ -537,6 +538,13 @@ async function handleSend(content, files, agentInputs) {
|
|
|
537
538
|
const prepared = composerExtension?.prepareSend?.(content);
|
|
538
539
|
const outbound = prepared?.content ?? content;
|
|
539
540
|
const resolvedAgentInputs = resolveTelaAgentInputs(agentInputs, embedConfig?.telaAgentInputs?.value);
|
|
541
|
+
const sent = await dispatchMessage(outbound, files, resolvedAgentInputs);
|
|
542
|
+
if (sent)
|
|
543
|
+
prepared?.commit?.();
|
|
544
|
+
if (!sent)
|
|
545
|
+
restoreQueuedSend({ content: prepared?.retryContent?.() ?? content, files, agentInputs: resolvedAgentInputs });
|
|
546
|
+
}
|
|
547
|
+
async function dispatchMessage(outbound, files, resolvedAgentInputs) {
|
|
540
548
|
let claimedWakeSessionId;
|
|
541
549
|
const sent = await sendMessage(outbound, files.length > 0 ? files : void 0, resolvedAgentInputs, async (signal) => {
|
|
542
550
|
if (!activeConversationId.value || !currentConversation.value) {
|
|
@@ -586,10 +594,7 @@ async function handleSend(content, files, agentInputs) {
|
|
|
586
594
|
});
|
|
587
595
|
if (sent && claimedWakeSessionId)
|
|
588
596
|
agentWake.consume();
|
|
589
|
-
|
|
590
|
-
prepared?.commit?.();
|
|
591
|
-
if (!sent)
|
|
592
|
-
restoreQueuedSend({ content: prepared?.retryContent?.() ?? content, files, agentInputs: resolvedAgentInputs });
|
|
597
|
+
return sent;
|
|
593
598
|
}
|
|
594
599
|
watch(
|
|
595
600
|
[outboundOccupied, agentWake.state],
|
|
@@ -614,6 +619,14 @@ watch(
|
|
|
614
619
|
void agentWake.wake(sessionId, selectedModel.value);
|
|
615
620
|
}
|
|
616
621
|
);
|
|
622
|
+
const sendChatMessageFromContent = async (content) => {
|
|
623
|
+
const trimmed = content.trim();
|
|
624
|
+
if (!trimmed || props.composerDisabled || composerLocked.value)
|
|
625
|
+
return false;
|
|
626
|
+
const resolvedAgentInputs = resolveTelaAgentInputs(void 0, embedConfig?.telaAgentInputs?.value);
|
|
627
|
+
return Boolean(await dispatchMessage(trimmed, [], resolvedAgentInputs));
|
|
628
|
+
};
|
|
629
|
+
provide(CHAT_MESSAGE_SEND_KEY, sendChatMessageFromContent);
|
|
617
630
|
async function handleCancel() {
|
|
618
631
|
await cancelConversation();
|
|
619
632
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { MentionItem } from '../../types/schemas/chat/ui.js';
|
|
2
|
+
import type { ChatComposerCommand } from '../../types/composer.js';
|
|
2
3
|
type MentionDropdownItem = MentionItem & {
|
|
3
4
|
iconUrl?: string;
|
|
4
5
|
};
|
|
5
6
|
type __VLS_Props = {
|
|
6
7
|
items: MentionDropdownItem[];
|
|
8
|
+
/** Host commands listed before the mention sections on the "/" trigger. */
|
|
9
|
+
commands?: readonly ChatComposerCommand[];
|
|
10
|
+
/** Indexes the combined [commands, items] list. */
|
|
7
11
|
highlightedIndex: number;
|
|
8
12
|
trigger: '@' | '/';
|
|
9
13
|
query: string;
|
|
@@ -12,8 +16,10 @@ type __VLS_Props = {
|
|
|
12
16
|
};
|
|
13
17
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
14
18
|
select: (item: MentionItem) => any;
|
|
19
|
+
selectCommand: (command: ChatComposerCommand) => any;
|
|
15
20
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
21
|
onSelect?: ((item: MentionItem) => any) | undefined;
|
|
22
|
+
onSelectCommand?: ((command: ChatComposerCommand) => any) | undefined;
|
|
17
23
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
24
|
declare const _default: typeof __VLS_export;
|
|
19
25
|
export default _default;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
const props = defineProps({
|
|
3
3
|
items: { type: Array, required: true },
|
|
4
|
+
commands: { type: Array, required: false },
|
|
4
5
|
highlightedIndex: { type: Number, required: true },
|
|
5
6
|
trigger: { type: String, required: true },
|
|
6
7
|
query: { type: String, required: true },
|
|
7
8
|
caretRect: { type: null, required: false }
|
|
8
9
|
});
|
|
9
|
-
const emit = defineEmits(["select"]);
|
|
10
|
+
const emit = defineEmits(["select", "selectCommand"]);
|
|
10
11
|
const listRef = ref(null);
|
|
11
12
|
const panelStyle = computed(() => {
|
|
12
13
|
const rect = props.caretRect;
|
|
@@ -18,20 +19,24 @@ const panelStyle = computed(() => {
|
|
|
18
19
|
bottom: `${window.innerHeight - rect.top + 4}px`
|
|
19
20
|
};
|
|
20
21
|
});
|
|
22
|
+
const commandEntries = computed(
|
|
23
|
+
() => (props.commands ?? []).map((command, i) => ({ command, globalIndex: i }))
|
|
24
|
+
);
|
|
21
25
|
const sections = computed(() => {
|
|
26
|
+
const offset = commandEntries.value.length;
|
|
22
27
|
if (props.trigger === "@") {
|
|
23
28
|
return [{
|
|
24
29
|
title: "Arquivos",
|
|
25
|
-
items: props.items.map((item, i) => ({ item, globalIndex: i }))
|
|
30
|
+
items: props.items.map((item, i) => ({ item, globalIndex: i + offset }))
|
|
26
31
|
}];
|
|
27
32
|
}
|
|
28
33
|
const canvasItems = [];
|
|
29
34
|
const skillItems = [];
|
|
30
35
|
props.items.forEach((item, i) => {
|
|
31
36
|
if (item.category === "canvas")
|
|
32
|
-
canvasItems.push({ item, globalIndex: i });
|
|
37
|
+
canvasItems.push({ item, globalIndex: i + offset });
|
|
33
38
|
else
|
|
34
|
-
skillItems.push({ item, globalIndex: i });
|
|
39
|
+
skillItems.push({ item, globalIndex: i + offset });
|
|
35
40
|
});
|
|
36
41
|
const result = [];
|
|
37
42
|
if (canvasItems.length > 0)
|
|
@@ -40,6 +45,7 @@ const sections = computed(() => {
|
|
|
40
45
|
result.push({ title: "Skills", items: skillItems });
|
|
41
46
|
return result;
|
|
42
47
|
});
|
|
48
|
+
const isEmpty = computed(() => props.items.length === 0 && commandEntries.value.length === 0);
|
|
43
49
|
const emptyMessage = computed(() => {
|
|
44
50
|
if (props.query)
|
|
45
51
|
return "Nenhum resultado encontrado";
|
|
@@ -67,11 +73,37 @@ watch(() => props.highlightedIndex, () => {
|
|
|
67
73
|
bg
|
|
68
74
|
p-4px
|
|
69
75
|
>
|
|
70
|
-
<div v-if="
|
|
76
|
+
<div v-if="isEmpty" px-12px py-6px>
|
|
71
77
|
<span body-14-regular text-tertiary>{{ emptyMessage }}</span>
|
|
72
78
|
</div>
|
|
73
79
|
|
|
74
80
|
<div v-else ref="listRef" flex="~ col">
|
|
81
|
+
<template v-if="commandEntries.length > 0">
|
|
82
|
+
<div px-12px pb-2px>
|
|
83
|
+
<span body-12-medium text-tertiary>Comandos</span>
|
|
84
|
+
</div>
|
|
85
|
+
<button
|
|
86
|
+
v-for="entry in commandEntries"
|
|
87
|
+
:key="entry.command.id"
|
|
88
|
+
type="button"
|
|
89
|
+
:data-highlighted="entry.globalIndex === highlightedIndex"
|
|
90
|
+
flex items-center gap-8px
|
|
91
|
+
w-full min-w-0
|
|
92
|
+
rounded-10px
|
|
93
|
+
px-12px py-4px
|
|
94
|
+
text-left
|
|
95
|
+
cursor-pointer select-none
|
|
96
|
+
outline-none
|
|
97
|
+
:class="entry.globalIndex === highlightedIndex ? 'bg-muted' : 'hover:bg-muted'"
|
|
98
|
+
@mousedown.prevent="emit('selectCommand', entry.command)"
|
|
99
|
+
>
|
|
100
|
+
<TelaIcon :name="entry.command.icon ?? 'i-ph-terminal-window'" size="18px" color="icon-secondary" shrink-0 />
|
|
101
|
+
<div flex="~ col" min-w-0>
|
|
102
|
+
<span body-12-medium text-primary truncate>/{{ entry.command.id }}</span>
|
|
103
|
+
<span body-10-regular leading-12px text-tertiary truncate>{{ entry.command.description ?? entry.command.label }}</span>
|
|
104
|
+
</div>
|
|
105
|
+
</button>
|
|
106
|
+
</template>
|
|
75
107
|
<template v-for="section in sections" :key="section.title">
|
|
76
108
|
<div px-12px pb-2px>
|
|
77
109
|
<span body-12-medium text-tertiary>{{ section.title }}</span>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { MentionItem } from '../../types/schemas/chat/ui.js';
|
|
2
|
+
import type { ChatComposerCommand } from '../../types/composer.js';
|
|
2
3
|
type MentionDropdownItem = MentionItem & {
|
|
3
4
|
iconUrl?: string;
|
|
4
5
|
};
|
|
5
6
|
type __VLS_Props = {
|
|
6
7
|
items: MentionDropdownItem[];
|
|
8
|
+
/** Host commands listed before the mention sections on the "/" trigger. */
|
|
9
|
+
commands?: readonly ChatComposerCommand[];
|
|
10
|
+
/** Indexes the combined [commands, items] list. */
|
|
7
11
|
highlightedIndex: number;
|
|
8
12
|
trigger: '@' | '/';
|
|
9
13
|
query: string;
|
|
@@ -12,8 +16,10 @@ type __VLS_Props = {
|
|
|
12
16
|
};
|
|
13
17
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
14
18
|
select: (item: MentionItem) => any;
|
|
19
|
+
selectCommand: (command: ChatComposerCommand) => any;
|
|
15
20
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
16
21
|
onSelect?: ((item: MentionItem) => any) | undefined;
|
|
22
|
+
onSelectCommand?: ((command: ChatComposerCommand) => any) | undefined;
|
|
17
23
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
24
|
declare const _default: typeof __VLS_export;
|
|
19
25
|
export default _default;
|
|
@@ -2,6 +2,8 @@ import type { Message } from '../../types/schemas/chat/conversations.js';
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
message: Message;
|
|
4
4
|
streamActive?: boolean;
|
|
5
|
+
/** True while this is the newest message in the conversation. */
|
|
6
|
+
isLatestMessage?: boolean;
|
|
5
7
|
/**
|
|
6
8
|
* Stand-in steps shown while this turn is still working towards its first
|
|
7
9
|
* output. They yield as soon as the run emits real reasoning or content.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useChatAuth } from "#chat-auth";
|
|
3
3
|
import { useEmbedConfig } from "../../embed/composables/embed-config";
|
|
4
|
+
import { CHAT_MESSAGE_CONTEXT_KEY } from "../../types/messages";
|
|
4
5
|
import { useWorkspaceMembers } from "../../workspace/composables/workspace-members";
|
|
5
6
|
import { isPromptTooLongFailure } from "../../types/schemas/chat/messages.js";
|
|
6
7
|
import { linkifyBareCitations } from "../../citations/citation-links";
|
|
7
8
|
import { parseContentWithFiles } from "../../files/content-detection";
|
|
9
|
+
import { renderEscapedUserMentions } from "../user-mentions";
|
|
10
|
+
import { parseUserCitations } from "../user-citations";
|
|
8
11
|
import ChatVaultLink from "../../files/components/chat-vault-link.vue";
|
|
9
12
|
const props = defineProps({
|
|
10
13
|
message: { type: null, required: true },
|
|
11
14
|
streamActive: { type: Boolean, required: false },
|
|
15
|
+
isLatestMessage: { type: Boolean, required: false },
|
|
12
16
|
preparingSteps: { type: [Array, null], required: false },
|
|
13
17
|
showTtft: { type: Boolean, required: false, default: true }
|
|
14
18
|
});
|
|
@@ -17,6 +21,12 @@ defineSlots();
|
|
|
17
21
|
const { user } = useChatAuth();
|
|
18
22
|
const { getMemberByEmail } = useWorkspaceMembers();
|
|
19
23
|
const embedConfig = useEmbedConfig();
|
|
24
|
+
provide(CHAT_MESSAGE_CONTEXT_KEY, {
|
|
25
|
+
messageId: props.message.id,
|
|
26
|
+
role: props.message.role,
|
|
27
|
+
isLatestMessage: computed(() => props.isLatestMessage === true),
|
|
28
|
+
streaming: computed(() => props.streamActive === true)
|
|
29
|
+
});
|
|
20
30
|
const mergedCustomComponents = computed(() => ({
|
|
21
31
|
link: ChatVaultLink,
|
|
22
32
|
...embedConfig?.customComponents.value ?? {}
|
|
@@ -64,28 +74,14 @@ const displayContent = computed(() => {
|
|
|
64
74
|
return content;
|
|
65
75
|
return linkifyBareCitations(content);
|
|
66
76
|
});
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return `<span class="mention-tag mention-tag-file"><span class="i-ph-note-blank" aria-hidden="true"></span>${safeName}</span>`;
|
|
76
|
-
}).replace(/<canvas_mention name="((?:[^&]|&(?!quot;))+)"[\s\S]*?<\/canvas_mention>/g, (_match, encodedName) => {
|
|
77
|
-
const name = encodedName.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"');
|
|
78
|
-
const safeName = name.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
79
|
-
return `<span class="mention-tag mention-tag-canvas"><span class="i-ph-wrench" aria-hidden="true"></span>${safeName}</span>`;
|
|
80
|
-
}).replace(/<skill_mention name="((?:[^&]|&(?!quot;))+)"[\s\S]*?<\/skill_mention>/g, (_match, encodedName) => {
|
|
81
|
-
const name = encodedName.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"');
|
|
82
|
-
const safeName = name.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
83
|
-
return `<span class="mention-tag mention-tag-skill"><span class="i-ph-hexagon" aria-hidden="true"></span>${safeName}</span>`;
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
const hasUserMentions = computed(
|
|
87
|
-
() => props.message.role === "user" && (displayContent.value.includes("<file_mention") || displayContent.value.includes("<canvas_mention") || displayContent.value.includes("<skill_mention"))
|
|
88
|
-
);
|
|
77
|
+
const userContentSegments = computed(() => parseUserCitations(
|
|
78
|
+
props.message.role === "user" ? props.message.content ?? "" : "",
|
|
79
|
+
Boolean(embedConfig?.citations.value)
|
|
80
|
+
));
|
|
81
|
+
function renderUserText(content) {
|
|
82
|
+
const escaped = content.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
83
|
+
return renderEscapedUserMentions(escaped);
|
|
84
|
+
}
|
|
89
85
|
const formattedTtft = computed(() => {
|
|
90
86
|
if (props.message.role !== "assistant" || !props.message.ttft)
|
|
91
87
|
return null;
|
|
@@ -158,9 +154,11 @@ const hasDetectedFiles = computed(() => contentSegments.value.some((s) => s.type
|
|
|
158
154
|
<span truncate max-w-200px>{{ file.name }}</span>
|
|
159
155
|
</a>
|
|
160
156
|
</div>
|
|
161
|
-
<div
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
<div body-16-regular whitespace-pre-wrap>
|
|
158
|
+
<template v-for="(segment, index) in userContentSegments" :key="`${index}-${segment.type === 'citation' ? segment.citation.raw : 'text'}`">
|
|
159
|
+
<ChatCitation v-if="segment.type === 'citation'" :citation="segment.citation" :label="segment.label" />
|
|
160
|
+
<span v-else v-html="renderUserText(segment.content)" />
|
|
161
|
+
</template>
|
|
164
162
|
</div>
|
|
165
163
|
</TelaChatMessageContent>
|
|
166
164
|
|
|
@@ -2,6 +2,8 @@ import type { Message } from '../../types/schemas/chat/conversations.js';
|
|
|
2
2
|
type __VLS_Props = {
|
|
3
3
|
message: Message;
|
|
4
4
|
streamActive?: boolean;
|
|
5
|
+
/** True while this is the newest message in the conversation. */
|
|
6
|
+
isLatestMessage?: boolean;
|
|
5
7
|
/**
|
|
6
8
|
* Stand-in steps shown while this turn is still working towards its first
|
|
7
9
|
* output. They yield as soon as the run emits real reasoning or content.
|
|
@@ -115,10 +115,12 @@ const skillItems = computed(() => {
|
|
|
115
115
|
}
|
|
116
116
|
return items;
|
|
117
117
|
});
|
|
118
|
+
const commandItems = computed(() => composerExtension?.commands ?? []);
|
|
118
119
|
const mentionAutocomplete = useMentionAutocomplete({
|
|
119
120
|
editorRef,
|
|
120
121
|
fileItems,
|
|
121
122
|
skillItems,
|
|
123
|
+
commandItems,
|
|
122
124
|
disabled: isLocked,
|
|
123
125
|
onInsert: handleInput
|
|
124
126
|
});
|
|
@@ -657,11 +659,13 @@ defineExpose({
|
|
|
657
659
|
<ChatMentionDropdown
|
|
658
660
|
v-if="mentionAutocomplete.isOpen"
|
|
659
661
|
:items="mentionAutocomplete.filteredItems"
|
|
662
|
+
:commands="mentionAutocomplete.filteredCommands"
|
|
660
663
|
:highlighted-index="mentionAutocomplete.highlightedIndex"
|
|
661
664
|
:trigger="mentionAutocomplete.activeTrigger"
|
|
662
665
|
:query="mentionAutocomplete.query"
|
|
663
666
|
:caret-rect="mentionAutocomplete.triggerCaretRect"
|
|
664
667
|
@select="mentionAutocomplete.selectItem"
|
|
668
|
+
@select-command="mentionAutocomplete.runCommand"
|
|
665
669
|
/>
|
|
666
670
|
|
|
667
671
|
<TelaChatPromptInput :locked="promptShellLocked" :class="centered && 'bg-white'">
|
|
@@ -40,6 +40,7 @@ const skeletonMessages = [
|
|
|
40
40
|
{ id: "assistant-2", align: "start", width: "84%", height: "92px" }
|
|
41
41
|
];
|
|
42
42
|
const timelineItems = computed(() => mergeConversationTimeline(props.messages, props.events));
|
|
43
|
+
const latestMessageId = computed(() => props.messages.at(-1)?.id ?? null);
|
|
43
44
|
const latestOutputSignal = computed(() => {
|
|
44
45
|
const latestMessage = props.messages.at(-1);
|
|
45
46
|
const latestEvent = props.events?.at(-1);
|
|
@@ -104,6 +105,7 @@ defineExpose({
|
|
|
104
105
|
v-else
|
|
105
106
|
:message="item.value"
|
|
106
107
|
:stream-active="item.id === streamingActiveMessageId"
|
|
108
|
+
:is-latest-message="item.id === latestMessageId"
|
|
107
109
|
:preparing-steps="item.value.role === 'assistant' && item.value.status === 'pending' ? preparingSteps : null"
|
|
108
110
|
:show-ttft="showTtft"
|
|
109
111
|
@retry="emit('retry', $event)"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { MentionItem } from '../../types/schemas/chat/ui.js';
|
|
2
|
+
import type { ChatComposerCommand } from '../../types/composer.js';
|
|
2
3
|
type UseMentionAutocompleteOptions = {
|
|
3
4
|
editorRef: Ref<HTMLElement | null>;
|
|
4
5
|
fileItems: ComputedRef<MentionItem[]>;
|
|
5
6
|
skillItems: ComputedRef<MentionItem[]>;
|
|
7
|
+
commandItems?: ComputedRef<readonly ChatComposerCommand[]>;
|
|
6
8
|
disabled?: Ref<boolean | undefined>;
|
|
7
9
|
onInsert?: () => void;
|
|
8
10
|
};
|
|
@@ -29,7 +29,7 @@ export function appendAttachmentMentions(content, files) {
|
|
|
29
29
|
return [content, ...tags].filter(Boolean).join("\n");
|
|
30
30
|
}
|
|
31
31
|
export function useMentionAutocomplete(options) {
|
|
32
|
-
const { editorRef, fileItems, skillItems, disabled, onInsert } = options;
|
|
32
|
+
const { editorRef, fileItems, skillItems, commandItems, disabled, onInsert } = options;
|
|
33
33
|
const activeTrigger = ref(null);
|
|
34
34
|
const query = ref("");
|
|
35
35
|
const highlightedIndex = ref(-1);
|
|
@@ -48,6 +48,17 @@ export function useMentionAutocomplete(options) {
|
|
|
48
48
|
(item) => item.label.toLowerCase().includes(q) || item.description?.toLowerCase().includes(q)
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
|
+
const filteredCommands = computed(() => {
|
|
52
|
+
if (activeTrigger.value !== "/")
|
|
53
|
+
return [];
|
|
54
|
+
const commands = commandItems?.value ?? [];
|
|
55
|
+
if (!query.value)
|
|
56
|
+
return commands;
|
|
57
|
+
const q = query.value.toLowerCase();
|
|
58
|
+
return commands.filter(
|
|
59
|
+
(command) => command.id.toLowerCase().includes(q) || command.label.toLowerCase().includes(q) || command.description?.toLowerCase().includes(q)
|
|
60
|
+
);
|
|
61
|
+
});
|
|
51
62
|
function getTextBeforeCursor() {
|
|
52
63
|
const sel = window.getSelection();
|
|
53
64
|
if (!sel || sel.rangeCount === 0 || !editorRef.value)
|
|
@@ -100,14 +111,15 @@ export function useMentionAutocomplete(options) {
|
|
|
100
111
|
triggerCaretRect.value = null;
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
114
|
+
const wasOpen = activeTrigger.value !== null;
|
|
115
|
+
const queryChanged = mentionTrigger.query !== lastQuery.value;
|
|
103
116
|
activeTrigger.value = mentionTrigger.trigger;
|
|
104
117
|
triggerTextOffset = mentionTrigger.offset;
|
|
105
118
|
triggerCaretRect.value = getCaretRangeAtOffset(triggerTextOffset)?.getBoundingClientRect() ?? null;
|
|
106
|
-
|
|
107
|
-
highlightedIndex.value = -1;
|
|
108
|
-
lastQuery.value = mentionTrigger.query;
|
|
109
|
-
}
|
|
119
|
+
lastQuery.value = mentionTrigger.query;
|
|
110
120
|
query.value = mentionTrigger.query;
|
|
121
|
+
if (!wasOpen || queryChanged)
|
|
122
|
+
highlightedIndex.value = filteredCommands.value.length > 0 ? 0 : -1;
|
|
111
123
|
}
|
|
112
124
|
function getCaretRangeAtOffset(offset) {
|
|
113
125
|
const editor = editorRef.value;
|
|
@@ -151,23 +163,35 @@ export function useMentionAutocomplete(options) {
|
|
|
151
163
|
}
|
|
152
164
|
return walkNode(editor);
|
|
153
165
|
}
|
|
154
|
-
function
|
|
166
|
+
function consumeTriggerText() {
|
|
155
167
|
if (!editorRef.value || triggerTextOffset < 0)
|
|
156
|
-
return;
|
|
168
|
+
return null;
|
|
157
169
|
const sel = window.getSelection();
|
|
158
170
|
if (!sel || sel.rangeCount === 0)
|
|
159
|
-
return;
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
return null;
|
|
172
|
+
const trigger = findMentionTrigger(getTextBeforeCursor());
|
|
173
|
+
if (!trigger || trigger.offset !== triggerTextOffset)
|
|
174
|
+
return null;
|
|
163
175
|
const triggerRange = getCaretRangeAtOffset(triggerTextOffset);
|
|
164
176
|
if (!triggerRange)
|
|
165
|
-
return;
|
|
177
|
+
return null;
|
|
166
178
|
const currentRange = sel.getRangeAt(0);
|
|
167
179
|
const deleteRange = document.createRange();
|
|
168
180
|
deleteRange.setStart(triggerRange.startContainer, triggerRange.startOffset);
|
|
169
181
|
deleteRange.setEnd(currentRange.startContainer, currentRange.startOffset);
|
|
170
182
|
deleteRange.deleteContents();
|
|
183
|
+
return deleteRange;
|
|
184
|
+
}
|
|
185
|
+
function selectItem(item) {
|
|
186
|
+
const textBeforeCursor = getTextBeforeCursor();
|
|
187
|
+
const charBeforeTrigger = triggerTextOffset > 0 ? textBeforeCursor[triggerTextOffset - 1] : null;
|
|
188
|
+
const needsSpaceBefore = charBeforeTrigger !== null && charBeforeTrigger !== " " && charBeforeTrigger !== "\n" && charBeforeTrigger !== "\xA0";
|
|
189
|
+
const deleteRange = consumeTriggerText();
|
|
190
|
+
const sel = window.getSelection();
|
|
191
|
+
if (!deleteRange || !sel) {
|
|
192
|
+
dismiss();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
171
195
|
const span = document.createElement("span");
|
|
172
196
|
span.className = `mention-tag mention-tag-${item.category}`;
|
|
173
197
|
span.contentEditable = "false";
|
|
@@ -202,6 +226,27 @@ export function useMentionAutocomplete(options) {
|
|
|
202
226
|
triggerTextOffset = -1;
|
|
203
227
|
onInsert?.();
|
|
204
228
|
}
|
|
229
|
+
function runCommand(command) {
|
|
230
|
+
const deleteRange = consumeTriggerText();
|
|
231
|
+
if (!deleteRange) {
|
|
232
|
+
dismiss();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const sel = window.getSelection();
|
|
236
|
+
if (sel) {
|
|
237
|
+
const caret = document.createRange();
|
|
238
|
+
caret.setStart(deleteRange.startContainer, deleteRange.startOffset);
|
|
239
|
+
caret.collapse(true);
|
|
240
|
+
sel.removeAllRanges();
|
|
241
|
+
sel.addRange(caret);
|
|
242
|
+
}
|
|
243
|
+
activeTrigger.value = null;
|
|
244
|
+
lastQuery.value = "";
|
|
245
|
+
highlightedIndex.value = -1;
|
|
246
|
+
triggerTextOffset = -1;
|
|
247
|
+
onInsert?.();
|
|
248
|
+
command.run();
|
|
249
|
+
}
|
|
205
250
|
function buildTaggedContent() {
|
|
206
251
|
if (!editorRef.value)
|
|
207
252
|
return "";
|
|
@@ -343,7 +388,8 @@ ${varLines}
|
|
|
343
388
|
dismiss();
|
|
344
389
|
return;
|
|
345
390
|
}
|
|
346
|
-
const
|
|
391
|
+
const commandCount = filteredCommands.value.length;
|
|
392
|
+
const count = commandCount + filteredItems.value.length;
|
|
347
393
|
if (count === 0)
|
|
348
394
|
return;
|
|
349
395
|
if (e.key === "ArrowDown") {
|
|
@@ -357,14 +403,16 @@ ${varLines}
|
|
|
357
403
|
return;
|
|
358
404
|
}
|
|
359
405
|
if (e.key === "Enter") {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
const index = highlightedIndex.value;
|
|
407
|
+
const command = index >= 0 ? filteredCommands.value[index] : void 0;
|
|
408
|
+
const item = index >= commandCount ? filteredItems.value[index - commandCount] : void 0;
|
|
409
|
+
e.preventDefault();
|
|
410
|
+
if (command)
|
|
411
|
+
runCommand(command);
|
|
412
|
+
else if (item)
|
|
363
413
|
selectItem(item);
|
|
364
|
-
|
|
365
|
-
e.preventDefault();
|
|
414
|
+
else
|
|
366
415
|
dismiss();
|
|
367
|
-
}
|
|
368
416
|
}
|
|
369
417
|
}
|
|
370
418
|
return reactive({
|
|
@@ -373,9 +421,11 @@ ${varLines}
|
|
|
373
421
|
query: readonly(query),
|
|
374
422
|
triggerCaretRect: readonly(triggerCaretRect),
|
|
375
423
|
filteredItems,
|
|
424
|
+
filteredCommands,
|
|
376
425
|
highlightedIndex,
|
|
377
426
|
handleKeydown,
|
|
378
427
|
selectItem,
|
|
428
|
+
runCommand,
|
|
379
429
|
dismiss,
|
|
380
430
|
detectTrigger,
|
|
381
431
|
buildTaggedContent,
|
|
@@ -1476,7 +1476,15 @@ export function createChatDebugScenarios(baseTime = Date.now()) {
|
|
|
1476
1476
|
message: message(baseTime, 0, {
|
|
1477
1477
|
id: "citations-user-message",
|
|
1478
1478
|
role: "user",
|
|
1479
|
-
content:
|
|
1479
|
+
content: [
|
|
1480
|
+
"A empresa pode monitorar os acessos dos empregados \xE0 internet?",
|
|
1481
|
+
"",
|
|
1482
|
+
"Documentos da base selecionados:",
|
|
1483
|
+
"- [S\xFAmula 331 do TST](cite://tst_sumulas/331)",
|
|
1484
|
+
"- [Parecer do BNDES](cite://bndes_pareceres/7154)",
|
|
1485
|
+
"",
|
|
1486
|
+
"Veja tamb\xE9m cite://stf_jurisprudencia/sjur336558."
|
|
1487
|
+
].join("\n")
|
|
1480
1488
|
})
|
|
1481
1489
|
}),
|
|
1482
1490
|
event({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CitationRef } from '../citations/citation-links.js';
|
|
2
|
+
export type UserCitationSegment = {
|
|
3
|
+
type: 'text';
|
|
4
|
+
content: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'citation';
|
|
7
|
+
citation: CitationRef;
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
/** Only citations become components; all other user content keeps its plain-text semantics. */
|
|
11
|
+
export declare function parseUserCitations(content: string, enabled: boolean): UserCitationSegment[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isCitationUrl, parseCitationUrl } from "../citations/citation-links.js";
|
|
2
|
+
const TOKENS = /<[a-z][a-z0-9-]*_mention\s[^>]*>[\s\S]*?<\/[a-z][a-z0-9-]*_mention>|(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:\1|$)|(`+)[^`]*?\2|!?\[((?:[^\[\]\n]|\[[^\]\n]*\])*)\]\(([^\s)]+)(?:\s+["'][^\n]*?["'])?\)|\[[ \t]*(cite:\/\/[^\s"'<>`\[\]()*~]+)[ \t]*\]|cite:\/\/[^\s"'<>`\[\]()*~]+/gi;
|
|
3
|
+
export function parseUserCitations(content, enabled) {
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return [{ type: "text", content }];
|
|
6
|
+
const segments = [];
|
|
7
|
+
let end = 0;
|
|
8
|
+
for (const match of content.matchAll(TOKENS)) {
|
|
9
|
+
const [raw, , , label, href, bracketed] = match;
|
|
10
|
+
if (raw.startsWith("!"))
|
|
11
|
+
continue;
|
|
12
|
+
const bare = !href && !bracketed && isCitationUrl(raw);
|
|
13
|
+
const url = href ?? bracketed ?? (bare ? raw.replace(/[.,:;!?]+$/, "") : "");
|
|
14
|
+
const citation = parseCitationUrl(url);
|
|
15
|
+
if (!citation)
|
|
16
|
+
continue;
|
|
17
|
+
const start = match.index;
|
|
18
|
+
if (start > end)
|
|
19
|
+
segments.push({ type: "text", content: content.slice(end, start) });
|
|
20
|
+
const visibleLabel = label?.trim();
|
|
21
|
+
segments.push({
|
|
22
|
+
type: "citation",
|
|
23
|
+
citation,
|
|
24
|
+
...visibleLabel && visibleLabel !== citation.id && !isCitationUrl(visibleLabel) ? { label: visibleLabel } : {}
|
|
25
|
+
});
|
|
26
|
+
end = start + (bare ? url.length : raw.length);
|
|
27
|
+
}
|
|
28
|
+
if (end < content.length || segments.length === 0)
|
|
29
|
+
segments.push({ type: "text", content: content.slice(end) });
|
|
30
|
+
return segments;
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function hasUserMentionTags(content: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Replaces `<*_mention name="...">...</...>` tags in already HTML-escaped user
|
|
4
|
+
* content with display pills, hiding the agent-facing instruction body. Known
|
|
5
|
+
* categories keep their icon and color; any other mention renders as a neutral
|
|
6
|
+
* pill, so host-defined mentions (e.g. template_mention) degrade gracefully.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderEscapedUserMentions(escaped: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const MENTION_ICONS = {
|
|
2
|
+
file: "i-ph-note-blank",
|
|
3
|
+
canvas: "i-ph-wrench",
|
|
4
|
+
skill: "i-ph-hexagon"
|
|
5
|
+
};
|
|
6
|
+
const DEFAULT_MENTION_ICON = "i-ph-paperclip";
|
|
7
|
+
const ESCAPED_MENTION_TAG = /<([a-z][a-z0-9-]*)_mention name="((?:[^&]|&(?!quot;))+)"[\s\S]*?<\/\1_mention>/g;
|
|
8
|
+
export function hasUserMentionTags(content) {
|
|
9
|
+
return /<[a-z][a-z0-9-]*_mention\s/.test(content);
|
|
10
|
+
}
|
|
11
|
+
export function renderEscapedUserMentions(escaped) {
|
|
12
|
+
return escaped.replace(ESCAPED_MENTION_TAG, (_match, category, encodedName) => {
|
|
13
|
+
const name = encodedName.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"');
|
|
14
|
+
const safeName = name.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
15
|
+
const known = category in MENTION_ICONS;
|
|
16
|
+
const icon = known ? MENTION_ICONS[category] : DEFAULT_MENTION_ICON;
|
|
17
|
+
const categoryClass = known ? `mention-tag-${category}` : "mention-tag-generic";
|
|
18
|
+
return `<span class="mention-tag ${categoryClass}"><span class="${icon}" aria-hidden="true"></span>${safeName}</span>`;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -9,6 +9,17 @@ export interface ChatPreparedSend {
|
|
|
9
9
|
/** Read after rejection. Defaults to the original composer text when omitted. */
|
|
10
10
|
retryContent?: () => string;
|
|
11
11
|
}
|
|
12
|
+
/** Local action listed under "Comandos" in the composer's "/" dropdown. */
|
|
13
|
+
export interface ChatComposerCommand {
|
|
14
|
+
/** Name matched against the text typed after "/", e.g. `templates`. */
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
/** Icon class rendered in the dropdown. Defaults to `i-ph-terminal-window`. */
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** Runs after the typed "/query" is removed from the draft. */
|
|
21
|
+
run: () => void;
|
|
22
|
+
}
|
|
12
23
|
/** Provide above MeistrariChatEmbed to extend its composer without replacing it. */
|
|
13
24
|
export interface ChatComposerExtension {
|
|
14
25
|
/** Action next to the file menu. Receives the composer's `disabled` state. */
|
|
@@ -19,4 +30,6 @@ export interface ChatComposerExtension {
|
|
|
19
30
|
hasAttachments?: MaybeRef<boolean>;
|
|
20
31
|
/** Prepare without consuming host state; commit after the send is accepted. */
|
|
21
32
|
prepareSend?: (content: string) => ChatPreparedSend | null | undefined;
|
|
33
|
+
/** Host commands offered alongside skills when the user types "/". */
|
|
34
|
+
commands?: readonly ChatComposerCommand[];
|
|
22
35
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
/** Stable string keys, also compatible with hosts injecting the literal key. */
|
|
3
|
+
export declare const CHAT_MESSAGE_SEND_KEY = "chat-message-send";
|
|
4
|
+
export declare const CHAT_MESSAGE_CONTEXT_KEY = "chat-message-context";
|
|
5
|
+
/**
|
|
6
|
+
* Provided by the embed to components rendered inside message content
|
|
7
|
+
* (`customComponents`). Sends `content` as the user through the same path as
|
|
8
|
+
* the composer, resolving the conversation's configured agent inputs — a
|
|
9
|
+
* direct `useChat().sendMessage` call would skip that resolution. Resolves
|
|
10
|
+
* `true` once the message is accepted (including into the outbound queue).
|
|
11
|
+
*/
|
|
12
|
+
export type ChatMessageSend = (content: string) => Promise<boolean>;
|
|
13
|
+
/** Provided per message bubble to components rendered inside its content. */
|
|
14
|
+
export interface ChatMessageContext {
|
|
15
|
+
messageId: string;
|
|
16
|
+
role: 'user' | 'assistant';
|
|
17
|
+
/** True while this is the newest message in the conversation. */
|
|
18
|
+
isLatestMessage: Readonly<Ref<boolean>>;
|
|
19
|
+
/** True while this message's turn is still streaming. */
|
|
20
|
+
streaming: Readonly<Ref<boolean>>;
|
|
21
|
+
}
|