@aphrody/m3-ai 3.3.2 → 3.3.5
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 +29 -7
- package/dist/A2APeerInspector.d.ts +41 -0
- package/dist/A2APeerInspector.d.ts.map +1 -0
- package/dist/A2ATaskTimeline.d.ts +28 -0
- package/dist/A2ATaskTimeline.d.ts.map +1 -0
- package/dist/AgUiEventStream.d.ts +87 -0
- package/dist/AgUiEventStream.d.ts.map +1 -0
- package/dist/GeminiAccountMenu.d.ts +55 -0
- package/dist/GeminiAccountMenu.d.ts.map +1 -0
- package/dist/GeminiPersonalContextEdit.d.ts +29 -0
- package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
- package/dist/GoogleSpotlightBar.d.ts +32 -0
- package/dist/GoogleSpotlightBar.d.ts.map +1 -0
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.d.ts.map +1 -1
- package/dist/McpAppCard.d.ts +24 -0
- package/dist/McpAppCard.d.ts.map +1 -0
- package/dist/ModelSelector.d.ts.map +1 -1
- package/dist/TaskRow.d.ts.map +1 -1
- package/dist/ToolCall.d.ts.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3629 -1950
- package/dist/index.js.map +32 -1
- package/package.json +17 -20
- package/src/A2APeerInspector.tsx +370 -0
- package/src/A2ATaskTimeline.tsx +218 -0
- package/src/AgUiEventStream.ts +492 -0
- package/src/ChatComposer.tsx +261 -0
- package/src/ConversationList.tsx +207 -0
- package/src/GeminiAccountMenu.tsx +212 -0
- package/src/GeminiPersonalContextEdit.tsx +147 -0
- package/src/GoogleSpotlightBar.tsx +165 -0
- package/src/InlineBranchCard.tsx +97 -0
- package/src/InlineWorkerCard.tsx +182 -0
- package/src/Markdown.tsx +769 -0
- package/src/McpAppCard.tsx +161 -0
- package/src/MemoryPanel.tsx +176 -0
- package/src/MessageBubble.tsx +143 -0
- package/src/ModelSelector.tsx +319 -0
- package/src/ModelUnavailable.tsx +116 -0
- package/src/TaskCreateForm.tsx +123 -0
- package/src/TaskDetail.tsx +224 -0
- package/src/TaskList.tsx +136 -0
- package/src/TaskPriorityIcon.tsx +39 -0
- package/src/TaskRow.tsx +177 -0
- package/src/TaskStatusIcon.tsx +39 -0
- package/src/ToolCall.tsx +644 -0
- package/src/chatIcons.tsx +105 -0
- package/src/chatLabels.ts +243 -0
- package/src/citations.tsx +286 -0
- package/src/index.ts +173 -0
- package/src/internal.tsx +107 -0
- package/src/openaiChatStream.ts +400 -0
- package/src/types.ts +212 -0
- package/src/useOpenAIChat.ts +126 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Inline Material Symbols (Outlined, weight 400) used by the chat components, so they render
|
|
3
|
+
// without the Material Symbols icon font. Paths from @material-symbols/svg-400 0.47.6
|
|
4
|
+
// (Google Material Symbols, Apache-2.0).
|
|
5
|
+
|
|
6
|
+
import { clsx } from "clsx";
|
|
7
|
+
|
|
8
|
+
const CHAT_ICON_PATHS = {
|
|
9
|
+
arrow_upward: [
|
|
10
|
+
"0 -960 960 960",
|
|
11
|
+
"M450-160v-526L202-438l-42-42 320-320 320 320-42 42-248-248v526h-60Z",
|
|
12
|
+
],
|
|
13
|
+
auto_awesome_filled: [
|
|
14
|
+
"0 96 960 960",
|
|
15
|
+
"m772 421-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333 862l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Z",
|
|
16
|
+
],
|
|
17
|
+
auto_awesome: [
|
|
18
|
+
"0 -960 960 960",
|
|
19
|
+
"m772-635-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333-194l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Zm0-148 48-96 98-43-98-43-48-96-47 96-99 43 99 43 47 96Zm0-139Z",
|
|
20
|
+
],
|
|
21
|
+
call_split: [
|
|
22
|
+
"0 -960 960 960",
|
|
23
|
+
"M452-160v-306L220-698v123h-60v-225h225v60H263l249 249v331h-60Zm113-364-42-42 174-174H575v-60h225v225h-60v-123L565-524Z",
|
|
24
|
+
],
|
|
25
|
+
check: ["0 -960 960 960", "M378-246 154-470l43-43 181 181 384-384 43 43-427 427Z"],
|
|
26
|
+
cloud_off: [
|
|
27
|
+
"0 -960 960 960",
|
|
28
|
+
"M818-56 703-171H248q-88 0-148-59T40-377q0-80 50.5-134T217-577q2-14 6.5-31.5T236-640L70-806l42-42L861-99l-43 43ZM248-231h397L285-591q-11 15-14.5 34t-3.5 37h-19q-62 0-105 39.5t-43 101q0 61.5 43 105T248-231Zm216-181Zm390 210-47-47q25-17 39-38t14-50q0-43-31-73.5T755-441h-67v-81q0-88-61-147.5T478.47-729q-28.47 0-60.97 9T358-691l-42-42q36-29 77.5-42.5T478-789q111 0 190.5 79T748-520v21q72-1 122 45t50 117q0 35-16.5 73.5T854-202ZM583-470Z",
|
|
29
|
+
],
|
|
30
|
+
content_copy: [
|
|
31
|
+
"0 -960 960 960",
|
|
32
|
+
"M300-200q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h440q24 0 42 18t18 42v560q0 24-18 42t-42 18H300Zm0-60h440v-560H300v560ZM180-80q-24 0-42-18t-18-42v-620h60v620h500v60H180Zm120-180v-560 560Z",
|
|
33
|
+
],
|
|
34
|
+
delete: [
|
|
35
|
+
"0 -960 960 960",
|
|
36
|
+
"M261-120q-24.75 0-42.37-17.63Q201-155.25 201-180v-570h-41v-60h188v-30h264v30h188v60h-41v570q0 24-18 42t-42 18H261Zm438-630H261v570h438v-570ZM367-266h60v-399h-60v399Zm166 0h60v-399h-60v399ZM261-750v570-570Z",
|
|
37
|
+
],
|
|
38
|
+
edit: [
|
|
39
|
+
"0 -960 960 960",
|
|
40
|
+
"M180-180h44l472-471-44-44-472 471v44Zm-60 60v-128l575-574q8-8 19-12.5t23-4.5q11 0 22 4.5t20 12.5l44 44q9 9 13 20t4 22q0 11-4.5 22.5T823-694L248-120H120Zm659-617-41-41 41 41Zm-105 64-22-22 44 44-22-22Z",
|
|
41
|
+
],
|
|
42
|
+
edit_square: [
|
|
43
|
+
"0 -960 960 960",
|
|
44
|
+
"M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h405l-60 60H180v600h600v-348l60-60v408q0 24-18 42t-42 18H180Zm300-360ZM360-360v-170l382-382q9-9 20-13t22-4q11 0 22.32 4.5Q817.63-920 827-911l83 84q8.61 8.96 13.3 19.78 4.7 10.83 4.7 22.02 0 11.2-4.5 22.7T910-742L530-360H360Zm508-425-84-84 84 84ZM420-420h85l253-253-43-42-43-42-252 251v86Zm295-295-43-42 43 42 43 42-43-42Z",
|
|
45
|
+
],
|
|
46
|
+
expand_more: ["0 -960 960 960", "M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z"],
|
|
47
|
+
info: [
|
|
48
|
+
"0 -960 960 960",
|
|
49
|
+
"M453-280h60v-240h-60v240Zm50.5-323.2q9.5-9.2 9.5-22.8 0-14.45-9.48-24.22-9.48-9.78-23.5-9.78t-23.52 9.78Q447-640.45 447-626q0 13.6 9.48 22.8 9.48 9.2 23.5 9.2t23.52-9.2ZM480.27-80q-82.74 0-155.5-31.5Q252-143 197.5-197.5t-86-127.34Q80-397.68 80-480.5t31.5-155.66Q143-709 197.5-763t127.34-85.5Q397.68-880 480.5-880t155.66 31.5Q709-817 763-763t85.5 127Q880-563 880-480.27q0 82.74-31.5 155.5Q817-252 763-197.68q-54 54.31-127 86Q563-80 480.27-80Zm.23-60Q622-140 721-239.5t99-241Q820-622 721.19-721T480-820q-141 0-240.5 98.81T140-480q0 141 99.5 240.5t241 99.5Zm-.5-340Z",
|
|
50
|
+
],
|
|
51
|
+
mic: [
|
|
52
|
+
"0 -960 960 960",
|
|
53
|
+
"M408-453.92q-29-30.91-29-75.08v-251q0-41.67 29.44-70.83Q437.88-880 479.94-880t71.56 29.17Q581-821.67 581-780v251q0 44.17-29 75.08Q523-423 480-423t-72-30.92ZM480-651Zm-30 531v-136q-106-11-178-89t-72-184h60q0 91 64.29 153t155.5 62q91.21 0 155.71-62Q700-438 700-529h60q0 106-72 184t-178 89v136h-60Zm59.5-376.5Q521-510 521-529v-251q0-17-11.79-28.5T480-820q-17.42 0-29.21 11.5T439-780v251q0 19 11.5 32.5T480-483q18 0 29.5-13.5Z",
|
|
54
|
+
],
|
|
55
|
+
open_in_new: [
|
|
56
|
+
"0 -960 960 960",
|
|
57
|
+
"M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h279v60H180v600h600v-279h60v279q0 24-18 42t-42 18H180Zm202-219-42-43 398-398H519v-60h321v321h-60v-218L382-339Z",
|
|
58
|
+
],
|
|
59
|
+
refresh: [
|
|
60
|
+
"0 -960 960 960",
|
|
61
|
+
"M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-129h60v254H546v-60h168q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q83 0 152-47.5T728-393h62q-29 105-115 169t-195 64Z",
|
|
62
|
+
],
|
|
63
|
+
search: [
|
|
64
|
+
"0 -960 960 960",
|
|
65
|
+
"M796-121 533-384q-30 26-70 40.5T378-329q-108 0-183-75t-75-181q0-106 75-181t182-75q106 0 180.5 75T632-585q0 43-14 83t-42 75l264 262-44 44ZM377-389q81 0 138-57.5T572-585q0-81-57-138.5T377-781q-82 0-139.5 57.5T180-585q0 81 57.5 138.5T377-389Z",
|
|
66
|
+
],
|
|
67
|
+
stop_filled: ["0 -960 960 960", "M240-240v-480h480v480H240Z"],
|
|
68
|
+
stop: ["0 -960 960 960", "M300-660v360-360Zm-60 420v-480h480v480H240Zm60-60h360v-360H300v360Z"],
|
|
69
|
+
thumb_down: [
|
|
70
|
+
"0 -960 960 960",
|
|
71
|
+
"M242-840h444v512L408-40l-39-31q-6-5-9-14t-3-22v-10l45-211H103q-24 0-42-18t-18-42v-81.84q0-7.16-1.5-14.66T43-499l126-290q8.88-21.25 29.59-36.13Q219.31-840 242-840Zm384 60H229L103-481v93h373l-53 249 203-214v-427Zm0 427v-427 427Zm60 25v-60h133v-392H686v-60h193v512H686Z",
|
|
72
|
+
],
|
|
73
|
+
} as const satisfies Record<string, readonly [string, string]>;
|
|
74
|
+
|
|
75
|
+
export type ChatIconName = keyof typeof CHAT_ICON_PATHS;
|
|
76
|
+
|
|
77
|
+
/** Names of the bundled chat icons. */
|
|
78
|
+
export const chatIconNames = Object.keys(CHAT_ICON_PATHS) as ChatIconName[];
|
|
79
|
+
|
|
80
|
+
export interface ChatIconProps {
|
|
81
|
+
name: ChatIconName;
|
|
82
|
+
/** Size in px. Default 24. */
|
|
83
|
+
size?: number;
|
|
84
|
+
className?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A decorative inline SVG Material Symbol painted with `currentColor`. */
|
|
88
|
+
export function ChatIcon({ name, size = 24, className }: ChatIconProps) {
|
|
89
|
+
const [viewBox, d] = CHAT_ICON_PATHS[name];
|
|
90
|
+
return (
|
|
91
|
+
<svg
|
|
92
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
93
|
+
viewBox={viewBox}
|
|
94
|
+
width={size}
|
|
95
|
+
height={size}
|
|
96
|
+
fill="currentColor"
|
|
97
|
+
aria-hidden="true"
|
|
98
|
+
focusable="false"
|
|
99
|
+
data-m3-icon={name}
|
|
100
|
+
className={clsx("m3-icon shrink-0", className)}
|
|
101
|
+
>
|
|
102
|
+
<path d={d} />
|
|
103
|
+
</svg>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Injectable labels of the chat surface: every visible string (aria-label included) of the chat
|
|
3
|
+
// components comes from this dictionary. French, English and Japanese are complete; a partial
|
|
4
|
+
// dictionary falls back key by key on the English defaults.
|
|
5
|
+
|
|
6
|
+
/** Every visible string of the chat components. `{name}` placeholders go through `formatLabel`. */
|
|
7
|
+
export interface ChatLabels {
|
|
8
|
+
// Composer
|
|
9
|
+
composerPlaceholder: string;
|
|
10
|
+
send: string;
|
|
11
|
+
stop: string;
|
|
12
|
+
openVoice: string;
|
|
13
|
+
projects: string;
|
|
14
|
+
// Model selector
|
|
15
|
+
models: string;
|
|
16
|
+
selectModel: string;
|
|
17
|
+
searchModels: string;
|
|
18
|
+
noModels: string;
|
|
19
|
+
/** `{count}`: context window, already formatted. */
|
|
20
|
+
contextTokens: string;
|
|
21
|
+
// Message actions
|
|
22
|
+
copyMessage: string;
|
|
23
|
+
copyStreamingMessage: string;
|
|
24
|
+
dislike: string;
|
|
25
|
+
fork: string;
|
|
26
|
+
assistantThinking: string;
|
|
27
|
+
// Code blocks
|
|
28
|
+
copy: string;
|
|
29
|
+
copyCode: string;
|
|
30
|
+
copied: string;
|
|
31
|
+
// Conversations
|
|
32
|
+
conversations: string;
|
|
33
|
+
newConversation: string;
|
|
34
|
+
searchConversations: string;
|
|
35
|
+
noConversations: string;
|
|
36
|
+
noSearchResults: string;
|
|
37
|
+
untitledConversation: string;
|
|
38
|
+
renameConversation: string;
|
|
39
|
+
deleteConversation: string;
|
|
40
|
+
// Model state
|
|
41
|
+
modelUnavailable: string;
|
|
42
|
+
modelDownloading: string;
|
|
43
|
+
modelLoading: string;
|
|
44
|
+
/** `{percent}`: integer 0..100. */
|
|
45
|
+
progressPercent: string;
|
|
46
|
+
retry: string;
|
|
47
|
+
// Citations and refusal
|
|
48
|
+
sources: string;
|
|
49
|
+
/** `{n}`: citation number. */
|
|
50
|
+
citation: string;
|
|
51
|
+
openSource: string;
|
|
52
|
+
refusalTitle: string;
|
|
53
|
+
refusalBody: string;
|
|
54
|
+
// Memory
|
|
55
|
+
memory: string;
|
|
56
|
+
memoryEmpty: string;
|
|
57
|
+
memoryLoading: string;
|
|
58
|
+
deleteMemory: string;
|
|
59
|
+
clearMemory: string;
|
|
60
|
+
clearMemoryConfirm: string;
|
|
61
|
+
confirm: string;
|
|
62
|
+
cancel: string;
|
|
63
|
+
// Generic
|
|
64
|
+
loading: string;
|
|
65
|
+
errorGeneric: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** English labels: the fallback of every key. */
|
|
69
|
+
export const chatLabelsEn: ChatLabels = {
|
|
70
|
+
composerPlaceholder: "Ask something...",
|
|
71
|
+
send: "Send",
|
|
72
|
+
stop: "Stop",
|
|
73
|
+
openVoice: "Open Voice Input",
|
|
74
|
+
projects: "Projects",
|
|
75
|
+
models: "Models",
|
|
76
|
+
selectModel: "Select model...",
|
|
77
|
+
searchModels: "Search models...",
|
|
78
|
+
noModels: "No models found",
|
|
79
|
+
contextTokens: "{count} tokens",
|
|
80
|
+
copyMessage: "Copy Message",
|
|
81
|
+
copyStreamingMessage: "Copy Streaming Message",
|
|
82
|
+
dislike: "Dislike",
|
|
83
|
+
fork: "Fork",
|
|
84
|
+
assistantThinking: "Thinking",
|
|
85
|
+
copy: "Copy",
|
|
86
|
+
copyCode: "Copy code",
|
|
87
|
+
copied: "Copied!",
|
|
88
|
+
conversations: "Conversations",
|
|
89
|
+
newConversation: "New conversation",
|
|
90
|
+
searchConversations: "Search conversations",
|
|
91
|
+
noConversations: "No conversations yet",
|
|
92
|
+
noSearchResults: "No matching conversation",
|
|
93
|
+
untitledConversation: "Untitled conversation",
|
|
94
|
+
renameConversation: "Rename conversation",
|
|
95
|
+
deleteConversation: "Delete conversation",
|
|
96
|
+
modelUnavailable: "Model unavailable",
|
|
97
|
+
modelDownloading: "Downloading the model",
|
|
98
|
+
modelLoading: "Loading the model",
|
|
99
|
+
progressPercent: "{percent}%",
|
|
100
|
+
retry: "Retry",
|
|
101
|
+
sources: "Sources",
|
|
102
|
+
citation: "Source {n}",
|
|
103
|
+
openSource: "Open source",
|
|
104
|
+
refusalTitle: "Not established in the sources",
|
|
105
|
+
refusalBody: "The available sources do not support an answer to this question.",
|
|
106
|
+
memory: "Memory",
|
|
107
|
+
memoryEmpty: "No memories saved",
|
|
108
|
+
memoryLoading: "Loading memories",
|
|
109
|
+
deleteMemory: "Delete memory",
|
|
110
|
+
clearMemory: "Clear all memories",
|
|
111
|
+
clearMemoryConfirm: "Delete every saved memory? This cannot be undone.",
|
|
112
|
+
confirm: "Confirm",
|
|
113
|
+
cancel: "Cancel",
|
|
114
|
+
loading: "Loading",
|
|
115
|
+
errorGeneric: "Something went wrong",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** French labels. */
|
|
119
|
+
export const chatLabelsFr: ChatLabels = {
|
|
120
|
+
composerPlaceholder: "Posez une question...",
|
|
121
|
+
send: "Envoyer",
|
|
122
|
+
stop: "Arrêter",
|
|
123
|
+
openVoice: "Saisie vocale",
|
|
124
|
+
projects: "Projets",
|
|
125
|
+
models: "Modèles",
|
|
126
|
+
selectModel: "Choisir un modèle...",
|
|
127
|
+
searchModels: "Rechercher un modèle...",
|
|
128
|
+
noModels: "Aucun modèle trouvé",
|
|
129
|
+
contextTokens: "{count} jetons",
|
|
130
|
+
copyMessage: "Copier le message",
|
|
131
|
+
copyStreamingMessage: "Copier le message en cours",
|
|
132
|
+
dislike: "Je n'aime pas",
|
|
133
|
+
fork: "Bifurquer à partir de ce tour",
|
|
134
|
+
assistantThinking: "Réflexion",
|
|
135
|
+
copy: "Copier",
|
|
136
|
+
copyCode: "Copier le code",
|
|
137
|
+
copied: "Copié !",
|
|
138
|
+
conversations: "Conversations",
|
|
139
|
+
newConversation: "Nouvelle conversation",
|
|
140
|
+
searchConversations: "Rechercher une conversation",
|
|
141
|
+
noConversations: "Aucune conversation pour l'instant",
|
|
142
|
+
noSearchResults: "Aucune conversation ne correspond",
|
|
143
|
+
untitledConversation: "Conversation sans titre",
|
|
144
|
+
renameConversation: "Renommer la conversation",
|
|
145
|
+
deleteConversation: "Supprimer la conversation",
|
|
146
|
+
modelUnavailable: "Modèle indisponible",
|
|
147
|
+
modelDownloading: "Téléchargement du modèle",
|
|
148
|
+
modelLoading: "Chargement du modèle",
|
|
149
|
+
progressPercent: "{percent} %",
|
|
150
|
+
retry: "Réessayer",
|
|
151
|
+
sources: "Sources",
|
|
152
|
+
citation: "Source {n}",
|
|
153
|
+
openSource: "Ouvrir la source",
|
|
154
|
+
refusalTitle: "Non établi dans les sources",
|
|
155
|
+
refusalBody: "Les sources disponibles ne permettent pas de répondre à cette question.",
|
|
156
|
+
memory: "Mémoire",
|
|
157
|
+
memoryEmpty: "Aucun souvenir enregistré",
|
|
158
|
+
memoryLoading: "Chargement des souvenirs",
|
|
159
|
+
deleteMemory: "Supprimer ce souvenir",
|
|
160
|
+
clearMemory: "Effacer tous les souvenirs",
|
|
161
|
+
clearMemoryConfirm: "Supprimer tous les souvenirs enregistrés ? Cette action est définitive.",
|
|
162
|
+
confirm: "Confirmer",
|
|
163
|
+
cancel: "Annuler",
|
|
164
|
+
loading: "Chargement",
|
|
165
|
+
errorGeneric: "Une erreur est survenue",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Japanese labels. */
|
|
169
|
+
export const chatLabelsJa: ChatLabels = {
|
|
170
|
+
composerPlaceholder: "質問を入力...",
|
|
171
|
+
send: "送信",
|
|
172
|
+
stop: "停止",
|
|
173
|
+
openVoice: "音声入力",
|
|
174
|
+
projects: "プロジェクト",
|
|
175
|
+
models: "モデル",
|
|
176
|
+
selectModel: "モデルを選択...",
|
|
177
|
+
searchModels: "モデルを検索...",
|
|
178
|
+
noModels: "モデルが見つかりません",
|
|
179
|
+
contextTokens: "{count} トークン",
|
|
180
|
+
copyMessage: "メッセージをコピー",
|
|
181
|
+
copyStreamingMessage: "生成中のメッセージをコピー",
|
|
182
|
+
dislike: "低評価",
|
|
183
|
+
fork: "このターンから分岐",
|
|
184
|
+
assistantThinking: "思考中",
|
|
185
|
+
copy: "コピー",
|
|
186
|
+
copyCode: "コードをコピー",
|
|
187
|
+
copied: "コピーしました",
|
|
188
|
+
conversations: "会話",
|
|
189
|
+
newConversation: "新しい会話",
|
|
190
|
+
searchConversations: "会話を検索",
|
|
191
|
+
noConversations: "まだ会話はありません",
|
|
192
|
+
noSearchResults: "一致する会話はありません",
|
|
193
|
+
untitledConversation: "無題の会話",
|
|
194
|
+
renameConversation: "会話の名前を変更",
|
|
195
|
+
deleteConversation: "会話を削除",
|
|
196
|
+
modelUnavailable: "モデルを利用できません",
|
|
197
|
+
modelDownloading: "モデルをダウンロード中",
|
|
198
|
+
modelLoading: "モデルを読み込み中",
|
|
199
|
+
progressPercent: "{percent}%",
|
|
200
|
+
retry: "再試行",
|
|
201
|
+
sources: "出典",
|
|
202
|
+
citation: "出典 {n}",
|
|
203
|
+
openSource: "出典を開く",
|
|
204
|
+
refusalTitle: "出典では確認できません",
|
|
205
|
+
refusalBody: "利用できる出典では、この質問に回答できません。",
|
|
206
|
+
memory: "メモリー",
|
|
207
|
+
memoryEmpty: "保存されたメモリーはありません",
|
|
208
|
+
memoryLoading: "メモリーを読み込み中",
|
|
209
|
+
deleteMemory: "メモリーを削除",
|
|
210
|
+
clearMemory: "すべてのメモリーを消去",
|
|
211
|
+
clearMemoryConfirm: "保存されたすべてのメモリーを削除しますか?この操作は元に戻せません。",
|
|
212
|
+
confirm: "確認",
|
|
213
|
+
cancel: "キャンセル",
|
|
214
|
+
loading: "読み込み中",
|
|
215
|
+
errorGeneric: "エラーが発生しました",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** The fallback dictionary (English). */
|
|
219
|
+
export const defaultChatLabels: ChatLabels = chatLabelsEn;
|
|
220
|
+
|
|
221
|
+
/** The complete dictionaries by language tag. */
|
|
222
|
+
export const chatLabelsByLang = { fr: chatLabelsFr, en: chatLabelsEn, ja: chatLabelsJa } as const;
|
|
223
|
+
|
|
224
|
+
/** Complete a partial dictionary with the defaults; empty strings fall back too. */
|
|
225
|
+
export function resolveChatLabels(
|
|
226
|
+
partial?: Partial<ChatLabels>,
|
|
227
|
+
base: ChatLabels = defaultChatLabels,
|
|
228
|
+
): ChatLabels {
|
|
229
|
+
if (!partial) return base;
|
|
230
|
+
const out = { ...base };
|
|
231
|
+
for (const key of Object.keys(partial) as (keyof ChatLabels)[]) {
|
|
232
|
+
const value = partial[key];
|
|
233
|
+
if (typeof value === "string" && value.length > 0) out[key] = value;
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Replace `{name}` placeholders of a label. Unknown placeholders stay as written. */
|
|
239
|
+
export function formatLabel(template: string, values: Record<string, string | number>): string {
|
|
240
|
+
return template.replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
241
|
+
name in values ? String(values[name]) : match,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Grounded answers: `[n]` markers rendered as citation chips, the numbered source list, and the
|
|
3
|
+
// refusal notice of a backend that declined to answer outside its sources.
|
|
4
|
+
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { clsx } from "clsx";
|
|
8
|
+
import { useMemo } from "react";
|
|
9
|
+
import type { Components } from "react-markdown";
|
|
10
|
+
|
|
11
|
+
import { type ChatLabels, formatLabel, resolveChatLabels } from "./chatLabels";
|
|
12
|
+
import { ChatIcon } from "./chatIcons";
|
|
13
|
+
import { focusRing } from "./internal";
|
|
14
|
+
import { Markdown, type MarkdownLinkHandler, externalLinkProps, isExternalHref } from "./Markdown";
|
|
15
|
+
|
|
16
|
+
/** One numbered passage an answer cites (`citations` of a grounded chat response). */
|
|
17
|
+
export interface Citation {
|
|
18
|
+
/** Marker number used in the text as `[n]`. */
|
|
19
|
+
n: number;
|
|
20
|
+
id?: string;
|
|
21
|
+
title: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
sourceId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CITATION_HREF = "#citation-";
|
|
27
|
+
const CODE_SPANS = /(```[\s\S]*?```|`[^`\n]*`)/;
|
|
28
|
+
const MARKER = /(?<![\]\\!])\[(\d{1,4})\](?!\()/g;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Rewrite the `[n]` markers of `content` whose number is cited into markdown links
|
|
32
|
+
* `[[n]](#citation-n)`. Code spans and fenced blocks are left untouched, as are markers
|
|
33
|
+
* without a matching citation.
|
|
34
|
+
*/
|
|
35
|
+
export function linkCitationMarkers(content: string, citations: readonly Citation[]): string {
|
|
36
|
+
if (citations.length === 0) return content;
|
|
37
|
+
const known = new Set(citations.map((citation) => citation.n));
|
|
38
|
+
return content
|
|
39
|
+
.split(CODE_SPANS)
|
|
40
|
+
.map((part, index) =>
|
|
41
|
+
index % 2 === 1
|
|
42
|
+
? part
|
|
43
|
+
: part.replace(MARKER, (match, digits: string) =>
|
|
44
|
+
known.has(Number(digits)) ? `[\\[${digits}\\]](${CITATION_HREF}${digits})` : match,
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The numbers cited in `content`, in order of first appearance. */
|
|
51
|
+
export function citedNumbers(content: string): number[] {
|
|
52
|
+
const seen: number[] = [];
|
|
53
|
+
for (const part of content.split(CODE_SPANS).filter((_, index) => index % 2 === 0)) {
|
|
54
|
+
for (const match of part.matchAll(MARKER)) {
|
|
55
|
+
const n = Number(match[1]);
|
|
56
|
+
if (!seen.includes(n)) seen.push(n);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return seen;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CitationChipProps {
|
|
63
|
+
citation: Citation;
|
|
64
|
+
labels?: Partial<ChatLabels>;
|
|
65
|
+
onSelect?: (citation: Citation) => void;
|
|
66
|
+
className?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Inline citation marker: a dense M3 assist chip showing `n`, labelled with the source title. */
|
|
70
|
+
export function CitationChip({ citation, labels, onSelect, className }: CitationChipProps) {
|
|
71
|
+
const l = resolveChatLabels(labels);
|
|
72
|
+
const label = `${formatLabel(l.citation, { n: citation.n })}: ${citation.title}`;
|
|
73
|
+
const chipClass = clsx(
|
|
74
|
+
"mx-0.5 inline-flex h-5 min-w-5 items-center justify-center rounded-small px-1 align-baseline",
|
|
75
|
+
"bg-secondary-container text-label-small text-on-secondary-container no-underline",
|
|
76
|
+
"transition-colors duration-200 ease-standard hover:bg-secondary-container/80",
|
|
77
|
+
focusRing,
|
|
78
|
+
className,
|
|
79
|
+
);
|
|
80
|
+
if (!onSelect && citation.url) {
|
|
81
|
+
return (
|
|
82
|
+
<a
|
|
83
|
+
href={citation.url}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noreferrer noopener"
|
|
86
|
+
className={chipClass}
|
|
87
|
+
aria-label={label}
|
|
88
|
+
title={citation.title}
|
|
89
|
+
>
|
|
90
|
+
{citation.n}
|
|
91
|
+
</a>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className={chipClass}
|
|
98
|
+
aria-label={label}
|
|
99
|
+
title={citation.title}
|
|
100
|
+
onClick={() => onSelect?.(citation)}
|
|
101
|
+
>
|
|
102
|
+
{citation.n}
|
|
103
|
+
</button>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Markdown overrides of `CitedMarkdown`: citation links become chips, other links stay safe. */
|
|
108
|
+
function citationComponents(
|
|
109
|
+
citations: readonly Citation[],
|
|
110
|
+
labels: Partial<ChatLabels> | undefined,
|
|
111
|
+
onCitationSelect: ((citation: Citation) => void) | undefined,
|
|
112
|
+
onLinkClick: MarkdownLinkHandler | undefined,
|
|
113
|
+
): Components {
|
|
114
|
+
const byNumber = new Map(citations.map((citation) => [citation.n, citation]));
|
|
115
|
+
return {
|
|
116
|
+
a: ({ node: _node, href, children, onClick, ...props }) => {
|
|
117
|
+
if (href?.startsWith(CITATION_HREF)) {
|
|
118
|
+
const citation = byNumber.get(Number(href.slice(CITATION_HREF.length)));
|
|
119
|
+
if (citation)
|
|
120
|
+
return <CitationChip citation={citation} labels={labels} onSelect={onCitationSelect} />;
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
<a
|
|
124
|
+
href={href}
|
|
125
|
+
{...(isExternalHref(href) ? externalLinkProps : {})}
|
|
126
|
+
onClick={(event) => {
|
|
127
|
+
onClick?.(event);
|
|
128
|
+
if (href) onLinkClick?.(href, event);
|
|
129
|
+
}}
|
|
130
|
+
className={clsx(
|
|
131
|
+
"rounded-extra-small text-primary underline underline-offset-2 hover:decoration-2",
|
|
132
|
+
focusRing,
|
|
133
|
+
)}
|
|
134
|
+
{...props}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</a>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface CitedMarkdownProps {
|
|
144
|
+
content: string;
|
|
145
|
+
citations: readonly Citation[];
|
|
146
|
+
labels?: Partial<ChatLabels>;
|
|
147
|
+
/** Called when a marker is activated; without it, markers with a `url` open it. */
|
|
148
|
+
onCitationSelect?: (citation: Citation) => void;
|
|
149
|
+
/** Activation of an ordinary link (not a citation marker); see `Markdown.onLinkClick`. */
|
|
150
|
+
onLinkClick?: MarkdownLinkHandler;
|
|
151
|
+
size?: "small" | "medium" | "large";
|
|
152
|
+
className?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Markdown whose `[n]` markers become citation chips. */
|
|
156
|
+
export function CitedMarkdown({
|
|
157
|
+
content,
|
|
158
|
+
citations,
|
|
159
|
+
labels,
|
|
160
|
+
onCitationSelect,
|
|
161
|
+
onLinkClick,
|
|
162
|
+
size = "large",
|
|
163
|
+
className,
|
|
164
|
+
}: CitedMarkdownProps) {
|
|
165
|
+
const components = useMemo(
|
|
166
|
+
() => citationComponents(citations, labels, onCitationSelect, onLinkClick),
|
|
167
|
+
[citations, labels, onCitationSelect, onLinkClick],
|
|
168
|
+
);
|
|
169
|
+
return (
|
|
170
|
+
<Markdown
|
|
171
|
+
content={linkCitationMarkers(content, citations)}
|
|
172
|
+
components={components}
|
|
173
|
+
labels={labels}
|
|
174
|
+
size={size}
|
|
175
|
+
className={className}
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface CitationListProps {
|
|
181
|
+
citations: readonly Citation[];
|
|
182
|
+
labels?: Partial<ChatLabels>;
|
|
183
|
+
onSelect?: (citation: Citation) => void;
|
|
184
|
+
className?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The numbered sources of an answer, as an M3 list on `surface-container-low`. */
|
|
188
|
+
export function CitationList({ citations, labels, onSelect, className }: CitationListProps) {
|
|
189
|
+
if (citations.length === 0) return null;
|
|
190
|
+
const l = resolveChatLabels(labels);
|
|
191
|
+
const sorted = [...citations];
|
|
192
|
+
sorted.sort((a, b) => a.n - b.n);
|
|
193
|
+
return (
|
|
194
|
+
<section
|
|
195
|
+
aria-label={l.sources}
|
|
196
|
+
className={clsx(
|
|
197
|
+
"m3-citation-list mt-2 rounded-medium bg-surface-container-low py-2",
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
<h3 className="px-4 pb-1 text-label-large text-on-surface-variant">{l.sources}</h3>
|
|
202
|
+
<ol className="m-0 list-none p-0">
|
|
203
|
+
{sorted.map((citation) => {
|
|
204
|
+
const body = (
|
|
205
|
+
<>
|
|
206
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded-small bg-secondary-container text-label-small text-on-secondary-container">
|
|
207
|
+
{citation.n}
|
|
208
|
+
</span>
|
|
209
|
+
<span className="min-w-0 flex-1 truncate text-body-medium text-on-surface">
|
|
210
|
+
{citation.title}
|
|
211
|
+
</span>
|
|
212
|
+
{citation.url || onSelect ? (
|
|
213
|
+
<ChatIcon name="open_in_new" size={18} className="text-on-surface-variant" />
|
|
214
|
+
) : null}
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
const rowClass = clsx(
|
|
218
|
+
"flex min-h-12 w-full items-center gap-3 px-4 py-1 text-left",
|
|
219
|
+
"transition-colors duration-200 ease-standard hover:bg-on-surface/8",
|
|
220
|
+
focusRing,
|
|
221
|
+
"focus-visible:outline-offset-[-3px]",
|
|
222
|
+
);
|
|
223
|
+
const name = `${formatLabel(l.citation, { n: citation.n })}: ${citation.title}`;
|
|
224
|
+
return (
|
|
225
|
+
<li
|
|
226
|
+
key={`${citation.n}-${citation.id ?? citation.title}`}
|
|
227
|
+
id={`citation-${citation.n}`}
|
|
228
|
+
>
|
|
229
|
+
{onSelect ? (
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
className={rowClass}
|
|
233
|
+
aria-label={name}
|
|
234
|
+
onClick={() => onSelect(citation)}
|
|
235
|
+
>
|
|
236
|
+
{body}
|
|
237
|
+
</button>
|
|
238
|
+
) : citation.url ? (
|
|
239
|
+
<a
|
|
240
|
+
href={citation.url}
|
|
241
|
+
target="_blank"
|
|
242
|
+
rel="noreferrer noopener"
|
|
243
|
+
className={rowClass}
|
|
244
|
+
aria-label={`${name} (${l.openSource})`}
|
|
245
|
+
>
|
|
246
|
+
{body}
|
|
247
|
+
</a>
|
|
248
|
+
) : (
|
|
249
|
+
<div className="flex min-h-12 items-center gap-3 px-4 py-1">{body}</div>
|
|
250
|
+
)}
|
|
251
|
+
</li>
|
|
252
|
+
);
|
|
253
|
+
})}
|
|
254
|
+
</ol>
|
|
255
|
+
</section>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface RefusalNoticeProps {
|
|
260
|
+
labels?: Partial<ChatLabels>;
|
|
261
|
+
/** Backend explanation, shown under the title instead of the generic body. */
|
|
262
|
+
detail?: string;
|
|
263
|
+
className?: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** `refusal: true`: the answer is not established in the sources (M3 tonal note, not an error). */
|
|
267
|
+
export function RefusalNotice({ labels, detail, className }: RefusalNoticeProps) {
|
|
268
|
+
const l = resolveChatLabels(labels);
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
role="note"
|
|
272
|
+
className={clsx(
|
|
273
|
+
"m3-refusal-notice flex items-start gap-3 rounded-medium bg-surface-container-high px-4 py-3",
|
|
274
|
+
className,
|
|
275
|
+
)}
|
|
276
|
+
>
|
|
277
|
+
<ChatIcon name="info" size={20} className="mt-0.5 text-on-surface-variant" />
|
|
278
|
+
<div className="min-w-0">
|
|
279
|
+
<p className="m-0 text-title-small text-on-surface">{l.refusalTitle}</p>
|
|
280
|
+
<p className="m-0 mt-0.5 text-body-medium text-on-surface-variant">
|
|
281
|
+
{detail || l.refusalBody}
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|