@djangocfg/widget-assistant 0.2.9 → 0.3.7
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 +31 -1
- package/package.json +7 -7
- package/src/AssistantChat.tsx +18 -6
- package/src/composer/ComposerBehaviour.tsx +17 -0
- package/src/composer/ComposerHelp.tsx +4 -1
- package/src/composer/earcons.ts +49 -0
- package/src/composer/handoff.ts +26 -0
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/ar.ts +3 -3
- package/src/i18n/locales/da.ts +3 -3
- package/src/i18n/locales/de.ts +3 -3
- package/src/i18n/locales/en.ts +3 -3
- package/src/i18n/locales/es.ts +3 -3
- package/src/i18n/locales/fr.ts +3 -3
- package/src/i18n/locales/it.ts +3 -3
- package/src/i18n/locales/ja.ts +3 -3
- package/src/i18n/locales/ko.ts +3 -3
- package/src/i18n/locales/nl.ts +3 -3
- package/src/i18n/locales/no.ts +3 -3
- package/src/i18n/locales/pl.ts +3 -3
- package/src/i18n/locales/pt-BR.ts +3 -3
- package/src/i18n/locales/ru.ts +3 -3
- package/src/i18n/locales/sv.ts +3 -3
- package/src/i18n/locales/tr.ts +3 -3
- package/src/i18n/locales/zh.ts +3 -3
- package/src/i18n/types.ts +4 -4
- package/src/index.ts +7 -4
- package/src/sessions/SessionsMenu.tsx +82 -0
- package/src/shell/AssistantFrame.tsx +11 -12
- package/src/shell/AssistantLocked.tsx +12 -4
- package/src/shell/ChatDock.tsx +16 -27
- package/src/stream/adapter.ts +8 -1
- package/src/stream/sessions.ts +58 -0
- package/src/stream/useSession.ts +111 -34
- package/src/wire/stored.ts +9 -0
- package/src/wire/transport.ts +13 -3
package/README.md
CHANGED
|
@@ -46,10 +46,40 @@ An unknown `type` is ignored. `wire/fixtures/events.json` holds one of each fram
|
|
|
46
46
|
|
|
47
47
|
`createTransport` takes `streamUrl` (a path, or a function of the send, for a thread id in the path), `buildBody`, `headers`, `streamFetch`, `getToken` (signed in) or `visitorToken` (anonymous, sent as `x-visitor-token`). Omit `api` from `useSession` for an anonymous host: nothing is fetched and the room starts empty. `context` is a plain record the host defines; the package never reads it. `examples/KbAssistant.tsx` is a knowledge-base host with a `citations` payload, the reference the tests mount.
|
|
48
48
|
|
|
49
|
+
## Sessions (0.3)
|
|
50
|
+
|
|
51
|
+
A session is one thread on the server. `useSession({ transport, api, storage, getContext })` shows one at a time and gives the host `startNew()`, `switchTo(id)`, `sessions` / `refreshSessions()` and `busy` (a turn is streaming: starting or switching waits, Stop ends it).
|
|
52
|
+
|
|
53
|
+
* `api: ThreadApi` is three calls the host maps to its endpoints: `list(signal)` (newest activity first), `create()` (`POST threads/`, always a new row) and `load(id, signal)` (throws `ThreadNotFoundError` on a 404: a stranger's or purged session).
|
|
54
|
+
* On load the remembered session (`storage: { read, write }`, the host picks the key; reads and writes must not throw) is shown, else the newest, else a blank page. **A session is created on the first send, never on load**; the transcript is not reseeded then, so the live message survives.
|
|
55
|
+
* `createTransport({ streamUrl: ({ threadId }) => … })` posts to the session in the path; `SendInput.threadId` is what `threadFor` returned.
|
|
56
|
+
* `<SessionsMenu session={session} />` is the header control: "New chat" and the list of earlier chats (title, "5 minutes ago", a check on the current one). Words come from the bundled dictionaries (`newChat`, `sessions`, `sessionsEmpty`).
|
|
57
|
+
* `<AssistantChat draftRoom={session.thread.session ?? 'new'} />` keeps each session's unsent draft under its own id.
|
|
58
|
+
* There is no delete and no `clear()`: "clear" is "new chat". Retention is the server's job.
|
|
59
|
+
* Without `api` (anonymous host) nothing is listed; `startNew()` just empties the room.
|
|
60
|
+
|
|
61
|
+
## Voice (0.3.3)
|
|
62
|
+
|
|
63
|
+
The microphone menu always offers a dictation language: a visitor may speak any language, whatever the host UI says. With no `speech` prop the assistant uses `useSpeechLanguage` from `@djangocfg/widget-chat/store` (the last pick, kept on this device; else the page language if the browser also lists it; else the browser's; else English). A host `speech={{ value, onChange }}` still overrides it.
|
|
64
|
+
|
|
65
|
+
Message sounds (sent, answer starts, answer done, error) come from `widget-chat` and are on by default; `messageSounds={false}` silences them, `messageSounds={{ volume, urls }}` tunes them.
|
|
66
|
+
|
|
67
|
+
Dictation start/stop tones are bundled (Web Audio, no files). `sounds={false}` silences them; `sounds={{ play(kind) }}` replaces them.
|
|
68
|
+
|
|
69
|
+
## Asking from outside, the dock and the locked room
|
|
70
|
+
|
|
71
|
+
* `askAssistant(text)` puts a prompt typed elsewhere on the page (a hero search, a "discuss this" button) into the composer and sends it once the composer is mounted and sending is allowed. One slot: asking twice before the composer exists keeps the last.
|
|
72
|
+
* `AssistantFrame` and `ChatDock` take `actions` (header controls such as `SessionsMenu`): they sit next to Close in the one panel header (`ChatPanelHeader` from `widget-chat`, drawn identically by the wide column, the tablet side panel and the phone sheet), set apart by a thin divider; each button has a ui-core tooltip. `ChatDock` is a side panel on tablets, the base package's `ChatSheet` on phones. The keys list (`ComposerHelp`) is hidden on touch devices.
|
|
73
|
+
* `AssistantLocked` takes `onComposerClick`: the inert composer becomes a button for the way out (sign in).
|
|
74
|
+
|
|
49
75
|
## Tests
|
|
50
76
|
|
|
51
77
|
`pnpm --filter @djangocfg/widget-assistant check test`: the wire, stream and boundary tests, and `tsc --noEmit`.
|
|
52
78
|
|
|
53
79
|
## Migrating from 0.1 (a host that spoke the film frames)
|
|
54
80
|
|
|
55
|
-
0.2 breaks the wire. In the host's server: `run` carries `thread_id` and `message_id` (not `run_id`, `user_message_id`, `assistant_message_id`); `status.phase` is `thinking|searching|answering` (`reading` and `choosing` are gone); `done` carries no message id; a card payload is `ui_payload {kind: 'film_cards', data}` as before, but the package no longer validates it, so register it: `dataRenderers={{ film_cards: … }}` and `payloadKinds={['film_cards']}`. A stored message has `parts: [{kind, data}]` in place of `widgets: [{kind, ids, caption}]`. `ChatContext` is a plain record (the old `title` and `browse` kinds are the host's to define). `createTransport` still opens `/apix/chat/v1/stream` when no `streamUrl` is given; that default
|
|
81
|
+
0.2 breaks the wire. In the host's server: `run` carries `thread_id` and `message_id` (not `run_id`, `user_message_id`, `assistant_message_id`); `status.phase` is `thinking|searching|answering` (`reading` and `choosing` are gone); `done` carries no message id; a card payload is `ui_payload {kind: 'film_cards', data}` as before, but the package no longer validates it, so register it: `dataRenderers={{ film_cards: … }}` and `payloadKinds={['film_cards']}`. A stored message has `parts: [{kind, data}]` in place of `widgets: [{kind, ids, caption}]`. `ChatContext` is a plain record (the old `title` and `browse` kinds are the host's to define). `createTransport` still opens `/apix/chat/v1/stream` when no `streamUrl` is given; that default stays for anonymous hosts only; a signed-in host with sessions sets `streamUrl`. The dictionaries lose `statusReading` and `statusChoosing` and gain `statusAnswering`; the bundled `lockedBody` and `hint` no longer talk about films.
|
|
82
|
+
|
|
83
|
+
## Migrating from 0.2 (one thread, `clear()`)
|
|
84
|
+
|
|
85
|
+
0.3 replaces the single-thread `ThreadApi { load, clear }` with `ThreadApi { list, create, load(id, signal) }` (`useSession` takes an optional `storage`), removes `Session.clear` (use `startNew`), swaps the dictionary keys `clear`, `clearConfirm`, `clearYes` for `newChat`, `sessions`, `sessionsEmpty`, and adds `ThreadNotFoundError`, the `not_found` reject code, `SendInput.threadId`, `AdapterDeps.threadFor` / `onActive`, `AssistantChat.draftRoom`, `SessionsMenu`, `useAssistantLocale` and `relativeTime`. A host that kept a hand-written sessions bar can delete it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/widget-assistant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "A site assistant on top of @djangocfg/widget-chat: the SSE wire and session layer, a persistent host, docked column, floating button and sheet, dictation, Help and 17 built-in locales",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chat",
|
|
@@ -44,19 +44,19 @@
|
|
|
44
44
|
"test": "vitest run"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@djangocfg/widget-chat": "^0.1.
|
|
47
|
+
"@djangocfg/widget-chat": "^0.1.23",
|
|
48
48
|
"zustand": "^5.0.16"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@djangocfg/i18n": "^2.1.
|
|
52
|
-
"@djangocfg/ui-core": "^2.1.
|
|
51
|
+
"@djangocfg/i18n": "^2.1.622",
|
|
52
|
+
"@djangocfg/ui-core": "^2.1.622",
|
|
53
53
|
"lucide-react": "^1.68.0",
|
|
54
54
|
"react": "^19.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@djangocfg/i18n": "^2.1.
|
|
58
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
59
|
-
"@djangocfg/ui-core": "^2.1.
|
|
57
|
+
"@djangocfg/i18n": "^2.1.622",
|
|
58
|
+
"@djangocfg/typescript-config": "^2.1.622",
|
|
59
|
+
"@djangocfg/ui-core": "^2.1.622",
|
|
60
60
|
"@storybook/react-vite": "^10.6.5",
|
|
61
61
|
"@types/node": "^25.9.5",
|
|
62
62
|
"@types/react": "19.3.0",
|
package/src/AssistantChat.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useRef, type ReactNode } from 'react';
|
|
4
4
|
|
|
5
|
+
import type { ChatSoundsCapability, VoiceSoundsCapability } from '@djangocfg/widget-chat/core';
|
|
6
|
+
import { useSpeechLanguage } from '@djangocfg/widget-chat/store';
|
|
5
7
|
import {
|
|
6
8
|
Chat,
|
|
7
9
|
ChatTranscript,
|
|
@@ -14,6 +16,8 @@ import {
|
|
|
14
16
|
import { Response } from '@djangocfg/widget-chat/content';
|
|
15
17
|
|
|
16
18
|
import type { SessionNotice } from './stream/useSession';
|
|
19
|
+
import { useAssistantLocale } from './i18n';
|
|
20
|
+
import { bundledEarcons } from './composer/earcons';
|
|
17
21
|
import { createWholeTranscriptSpeech } from './composer/wholeTranscript';
|
|
18
22
|
import { AssistantComposer } from './composer/AssistantComposer';
|
|
19
23
|
import { ComposerHelp } from './composer/ComposerHelp';
|
|
@@ -33,12 +37,18 @@ interface Props {
|
|
|
33
37
|
adapter: ChatAdapter;
|
|
34
38
|
/** Example prompts shown in an empty chat. */
|
|
35
39
|
suggestions: string[];
|
|
36
|
-
/** The room on screen: `id` changes when the thread is reloaded (reconcile,
|
|
40
|
+
/** The room on screen: `id` changes when the thread is reloaded (reconcile, a new or another session), which reseeds the transcript. */
|
|
37
41
|
conversation?: { id?: string; history: readonly ChatMessage[] };
|
|
42
|
+
/** What the unsent draft is kept under: the session's own id, so each session keeps its draft across reseeds. Absent: the conversation `id`. */
|
|
43
|
+
draftRoom?: string;
|
|
38
44
|
/** The host's drawing of structured parts, by `kind` (for example `citations`). Unknown kinds fall through to the widget. */
|
|
39
45
|
dataRenderers?: Readonly<Record<string, DataPartRenderer>>;
|
|
40
|
-
/** Dictation language and its picker (the microphone menu). Absent: the
|
|
46
|
+
/** Dictation language and its picker (the microphone menu). Absent: the assistant's own picker (the viewer's last pick, else the page and browser language), always shown. */
|
|
41
47
|
speech?: SpeechLanguage;
|
|
48
|
+
/** The dictation start/stop tones. Absent: the bundled ones (Web Audio, no files). `false`: silent. */
|
|
49
|
+
sounds?: VoiceSoundsCapability | false;
|
|
50
|
+
/** The message sounds (sent, answer starts, answer done, error). Absent: the widget's bundled ones, on. `false`: silent. */
|
|
51
|
+
messageSounds?: ChatSoundsCapability | false;
|
|
42
52
|
/** A plain line above the composer: reconnecting, or a stalled run with its Retry (`onRetry`). */
|
|
43
53
|
notice?: SessionNotice | null;
|
|
44
54
|
onRetry?: () => void;
|
|
@@ -57,7 +67,7 @@ const NO_RENDERERS: Readonly<Record<string, DataPartRenderer>> = {};
|
|
|
57
67
|
* recall of sent prompts with the arrow keys, an unsent draft that survives a reload). The widget's packaged layout shows only a bare composer, so the layout
|
|
58
68
|
* is composed here from its own primitives: every behaviour is the widget's, nothing is re-implemented.
|
|
59
69
|
*/
|
|
60
|
-
export function AssistantChat({ adapter, suggestions, conversation = EMPTY_ROOM, dataRenderers = NO_RENDERERS, speech, notice, onRetry, focusWhen, refocus, className }: Props) {
|
|
70
|
+
export function AssistantChat({ adapter, suggestions, conversation = EMPTY_ROOM, draftRoom, dataRenderers = NO_RENDERERS, speech: given, sounds, messageSounds, notice, onRetry, focusWhen, refocus, className }: Props) {
|
|
61
71
|
const renderers = useMemo(
|
|
62
72
|
() => ({
|
|
63
73
|
text: ({ part }: PartRenderProps) => (part.type === 'text' ? <Response>{part.text}</Response> : null),
|
|
@@ -72,13 +82,15 @@ export function AssistantChat({ adapter, suggestions, conversation = EMPTY_ROOM,
|
|
|
72
82
|
}),
|
|
73
83
|
[dataRenderers],
|
|
74
84
|
);
|
|
85
|
+
const own = useSpeechLanguage({ uiLocale: useAssistantLocale() });
|
|
86
|
+
const speech = given ?? { value: own.language, onChange: own.setLanguage };
|
|
75
87
|
// The language is read at each start, through a ref: the capability must not be rebuilt when the picker changes.
|
|
76
88
|
const language = useRef(speech?.value);
|
|
77
89
|
language.current = speech?.value;
|
|
78
90
|
const voice = useMemo(() => createWholeTranscriptSpeech(() => language.current ?? (typeof navigator === 'undefined' ? 'en-US' : navigator.language)), []);
|
|
79
91
|
const capabilities = useMemo<ChatCapabilities>(
|
|
80
|
-
() => ({ suggestions: { items: suggestions.map((prompt, index) => ({ id: `s${index}`, title: prompt, prompt })) }, ...(voice ? { voice } : {}) }),
|
|
81
|
-
[suggestions, voice],
|
|
92
|
+
() => ({ suggestions: { items: suggestions.map((prompt, index) => ({ id: `s${index}`, title: prompt, prompt })) }, ...(voice ? { voice } : {}), ...(sounds === false ? {} : { voiceSounds: sounds ?? bundledEarcons }), ...(messageSounds === undefined ? {} : { sounds: messageSounds }) }),
|
|
93
|
+
[suggestions, voice, sounds, messageSounds],
|
|
82
94
|
);
|
|
83
95
|
const slots = useMemo(
|
|
84
96
|
() => ({
|
|
@@ -106,7 +118,7 @@ export function AssistantChat({ adapter, suggestions, conversation = EMPTY_ROOM,
|
|
|
106
118
|
children: (
|
|
107
119
|
<>
|
|
108
120
|
<ChatTranscript footer={(message) => <MessageFooter message={message} />} />
|
|
109
|
-
<AssistantComposer speech={speech} draftRoom={conversation.id ?? 'assistant'} focusWhen={focusWhen} refocus={refocus} />
|
|
121
|
+
<AssistantComposer speech={speech} draftRoom={draftRoom ?? conversation.id ?? 'assistant'} focusWhen={focusWhen} refocus={refocus} />
|
|
110
122
|
</>
|
|
111
123
|
),
|
|
112
124
|
}}
|
|
@@ -6,6 +6,7 @@ import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
|
6
6
|
import { useComposer } from '@djangocfg/widget-chat';
|
|
7
7
|
|
|
8
8
|
import { useCaretFocus } from './useCaretFocus';
|
|
9
|
+
import { clearPendingPrompt, usePendingPrompt } from './handoff';
|
|
9
10
|
import { useComposerNodes } from './useComposerNodes';
|
|
10
11
|
import { DICTATION_TOGGLE } from './keys';
|
|
11
12
|
import { useFocusArea } from './useFocusArea';
|
|
@@ -33,6 +34,22 @@ export function ComposerBehaviour({ focusWhen = false, refocus = false }: Props)
|
|
|
33
34
|
const latest = useRef(composer);
|
|
34
35
|
latest.current = composer;
|
|
35
36
|
|
|
37
|
+
// A prompt asked from outside the assistant (`askAssistant`): into the draft, then sent once the draft holds it and sending is allowed.
|
|
38
|
+
const pendingPrompt = usePendingPrompt();
|
|
39
|
+
const draftText = composer.draft.text;
|
|
40
|
+
const canSend = composer.canSend;
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!pendingPrompt) return;
|
|
43
|
+
if (draftText !== pendingPrompt) {
|
|
44
|
+
latest.current.setText(pendingPrompt);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (canSend) {
|
|
48
|
+
clearPendingPrompt();
|
|
49
|
+
latest.current.send();
|
|
50
|
+
}
|
|
51
|
+
}, [pendingPrompt, draftText, canSend]);
|
|
52
|
+
|
|
36
53
|
const recording = useRecording(nodes);
|
|
37
54
|
useHeightFloor(nodes, recording);
|
|
38
55
|
useFocusArea(nodes, marker, refocus);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Kbd, Popover, PopoverContent, PopoverTrigger } from '@djangocfg/ui-core/components';
|
|
4
|
-
import { formatHotkey } from '@djangocfg/ui-core/hooks';
|
|
4
|
+
import { formatHotkey, useIsTouch } from '@djangocfg/ui-core/hooks';
|
|
5
5
|
|
|
6
6
|
import { useAssistantCopy } from '../i18n';
|
|
7
7
|
import { DICTATION_TOGGLE } from './keys';
|
|
@@ -22,6 +22,9 @@ function useRows() {
|
|
|
22
22
|
export function ComposerHelp() {
|
|
23
23
|
const copy = useAssistantCopy();
|
|
24
24
|
const rows = useRows();
|
|
25
|
+
const touch = useIsTouch();
|
|
26
|
+
// The keys are a desktop keyboard's: a phone has none of them.
|
|
27
|
+
if (touch) return null;
|
|
25
28
|
return (
|
|
26
29
|
<div className="px-4 pb-1 text-end">
|
|
27
30
|
<Popover>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { VoiceSoundsCapability } from '@djangocfg/widget-chat/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The dictation tones, made with the Web Audio API: a rising pair when the microphone opens, a falling pair when it closes. No audio files ship and none
|
|
5
|
+
* are fetched. A tone is quiet and short; a browser that has no `AudioContext`, or has not been unlocked by a gesture, plays nothing (the dictation starts from a click, so it is unlocked).
|
|
6
|
+
*/
|
|
7
|
+
const TONES: Readonly<Record<'start' | 'stop', readonly [number, number]>> = {
|
|
8
|
+
start: [660, 880],
|
|
9
|
+
stop: [880, 560],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const STEP = 0.07;
|
|
13
|
+
const GAIN = 0.06;
|
|
14
|
+
|
|
15
|
+
let context: AudioContext | null | undefined;
|
|
16
|
+
|
|
17
|
+
function audio(): AudioContext | null {
|
|
18
|
+
if (context !== undefined) return context;
|
|
19
|
+
const Ctor = typeof window === 'undefined' ? undefined : (window.AudioContext ?? (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext);
|
|
20
|
+
context = Ctor ? new Ctor() : null;
|
|
21
|
+
return context;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The bundled tones. Never throws: the session runs this on the path that opens the microphone. */
|
|
25
|
+
export const bundledEarcons: VoiceSoundsCapability = {
|
|
26
|
+
play: (kind) => {
|
|
27
|
+
try {
|
|
28
|
+
const ctx = audio();
|
|
29
|
+
if (!ctx) return;
|
|
30
|
+
if (ctx.state === 'suspended') void ctx.resume();
|
|
31
|
+
const [first, second] = TONES[kind];
|
|
32
|
+
const now = ctx.currentTime;
|
|
33
|
+
[first, second].forEach((frequency, index) => {
|
|
34
|
+
const osc = ctx.createOscillator();
|
|
35
|
+
const gain = ctx.createGain();
|
|
36
|
+
const at = now + index * STEP;
|
|
37
|
+
osc.frequency.value = frequency;
|
|
38
|
+
gain.gain.setValueAtTime(0, at);
|
|
39
|
+
gain.gain.linearRampToValueAtTime(GAIN, at + 0.01);
|
|
40
|
+
gain.gain.linearRampToValueAtTime(0, at + STEP);
|
|
41
|
+
osc.connect(gain).connect(ctx.destination);
|
|
42
|
+
osc.start(at);
|
|
43
|
+
osc.stop(at + STEP + 0.02);
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
// A tone is a convenience.
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A prompt typed outside the assistant (the header's AI mode) waiting for the composer. The composer, once mounted, puts it in the draft and
|
|
5
|
+
* sends it, then clears it. One slot: asking twice before the composer exists keeps the last.
|
|
6
|
+
*/
|
|
7
|
+
let pending: string | null = null;
|
|
8
|
+
const listeners = new Set<() => void>();
|
|
9
|
+
const emit = () => listeners.forEach((listener) => listener());
|
|
10
|
+
|
|
11
|
+
export function askAssistant(text: string): void {
|
|
12
|
+
pending = text.trim() || null;
|
|
13
|
+
emit();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clearPendingPrompt(): void {
|
|
17
|
+
pending = null;
|
|
18
|
+
emit();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const subscribe = (listener: () => void) => {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => void listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const usePendingPrompt = (): string | null => useSyncExternalStore(subscribe, () => pending, () => null);
|
package/src/i18n/index.ts
CHANGED
|
@@ -34,3 +34,6 @@ const kit = createCopy(ASSISTANT_DICTIONARIES);
|
|
|
34
34
|
* `@djangocfg/i18n` provider, else English. The host passes no strings.
|
|
35
35
|
*/
|
|
36
36
|
export const useAssistantCopy = kit.useCopy;
|
|
37
|
+
|
|
38
|
+
/** The bundled locale the assistant resolved to (feed it to `Intl`). */
|
|
39
|
+
export const useAssistantLocale = kit.useLocale;
|
package/src/i18n/locales/ar.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const ar: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'إعادة الاتصال…',
|
|
21
21
|
stalled: 'ما زالت الإجابة قيد الإعداد.',
|
|
22
22
|
retry: 'إعادة المحاولة',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'محادثة جديدة',
|
|
24
|
+
sessions: 'المحادثات السابقة',
|
|
25
|
+
sessionsEmpty: 'لا توجد محادثات سابقة بعد',
|
|
26
26
|
loadError: 'تعذّر تحميل المحادثة.',
|
|
27
27
|
helpLabel: 'مساعدة',
|
|
28
28
|
helpDictate: 'بدء الإملاء أو إيقافه',
|
package/src/i18n/locales/da.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const da: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Forbinder igen…',
|
|
21
21
|
stalled: 'Svaret er stadig undervejs.',
|
|
22
22
|
retry: 'Prøv igen',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Ny chat',
|
|
24
|
+
sessions: 'Tidligere chats',
|
|
25
|
+
sessionsEmpty: 'Ingen tidligere chats endnu',
|
|
26
26
|
loadError: 'Samtalen kunne ikke indlæses.',
|
|
27
27
|
helpLabel: 'Hjælp',
|
|
28
28
|
helpDictate: 'Start eller stop diktering',
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const de: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Verbinde neu…',
|
|
21
21
|
stalled: 'Die Antwort wird noch erstellt.',
|
|
22
22
|
retry: 'Erneut versuchen',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Neuer Chat',
|
|
24
|
+
sessions: 'Frühere Chats',
|
|
25
|
+
sessionsEmpty: 'Noch keine früheren Chats',
|
|
26
26
|
loadError: 'Die Unterhaltung konnte nicht geladen werden.',
|
|
27
27
|
helpLabel: 'Hilfe',
|
|
28
28
|
helpDictate: 'Diktat starten oder beenden',
|
package/src/i18n/locales/en.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const en: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Reconnecting…',
|
|
21
21
|
stalled: 'Still working on the answer.',
|
|
22
22
|
retry: 'Retry',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'New chat',
|
|
24
|
+
sessions: 'Earlier chats',
|
|
25
|
+
sessionsEmpty: 'No earlier chats yet',
|
|
26
26
|
loadError: 'Could not load the conversation.',
|
|
27
27
|
helpLabel: 'Help',
|
|
28
28
|
helpDictate: 'Start or stop dictation',
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const es: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Reconectando…',
|
|
21
21
|
stalled: 'La respuesta sigue en curso.',
|
|
22
22
|
retry: 'Reintentar',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Nuevo chat',
|
|
24
|
+
sessions: 'Chats anteriores',
|
|
25
|
+
sessionsEmpty: 'Aún no hay chats anteriores',
|
|
26
26
|
loadError: 'No se pudo cargar la conversación.',
|
|
27
27
|
helpLabel: 'Ayuda',
|
|
28
28
|
helpDictate: 'Iniciar o detener el dictado',
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const fr: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Reconnexion…',
|
|
21
21
|
stalled: 'La réponse est encore en cours.',
|
|
22
22
|
retry: 'Réessayer',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Nouvelle discussion',
|
|
24
|
+
sessions: 'Discussions précédentes',
|
|
25
|
+
sessionsEmpty: 'Aucune discussion précédente',
|
|
26
26
|
loadError: 'Impossible de charger la conversation.',
|
|
27
27
|
helpLabel: 'Aide',
|
|
28
28
|
helpDictate: 'Démarrer ou arrêter la dictée',
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const it: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Riconnessione…',
|
|
21
21
|
stalled: 'La risposta è ancora in corso.',
|
|
22
22
|
retry: 'Riprova',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Nuova chat',
|
|
24
|
+
sessions: 'Chat precedenti',
|
|
25
|
+
sessionsEmpty: 'Ancora nessuna chat precedente',
|
|
26
26
|
loadError: 'Impossibile caricare la conversazione.',
|
|
27
27
|
helpLabel: 'Aiuto',
|
|
28
28
|
helpDictate: 'Avvia o ferma la dettatura',
|
package/src/i18n/locales/ja.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const ja: AssistantCopy = {
|
|
|
20
20
|
reconnecting: '再接続中…',
|
|
21
21
|
stalled: 'まだ回答を作成中です。',
|
|
22
22
|
retry: '再試行',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: '新しいチャット',
|
|
24
|
+
sessions: '過去のチャット',
|
|
25
|
+
sessionsEmpty: '過去のチャットはまだありません',
|
|
26
26
|
loadError: '会話を読み込めませんでした。',
|
|
27
27
|
helpLabel: 'ヘルプ',
|
|
28
28
|
helpDictate: '音声入力の開始/停止',
|
package/src/i18n/locales/ko.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const ko: AssistantCopy = {
|
|
|
20
20
|
reconnecting: '다시 연결하는 중…',
|
|
21
21
|
stalled: '아직 답변을 준비 중입니다.',
|
|
22
22
|
retry: '다시 시도',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: '새 채팅',
|
|
24
|
+
sessions: '이전 채팅',
|
|
25
|
+
sessionsEmpty: '이전 채팅이 아직 없습니다',
|
|
26
26
|
loadError: '대화를 불러오지 못했습니다.',
|
|
27
27
|
helpLabel: '도움말',
|
|
28
28
|
helpDictate: '음성 입력 시작/중지',
|
package/src/i18n/locales/nl.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const nl: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Opnieuw verbinden…',
|
|
21
21
|
stalled: 'Het antwoord wordt nog gemaakt.',
|
|
22
22
|
retry: 'Opnieuw proberen',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Nieuwe chat',
|
|
24
|
+
sessions: 'Eerdere chats',
|
|
25
|
+
sessionsEmpty: 'Nog geen eerdere chats',
|
|
26
26
|
loadError: 'Het gesprek kon niet worden geladen.',
|
|
27
27
|
helpLabel: 'Help',
|
|
28
28
|
helpDictate: 'Dicteren starten of stoppen',
|
package/src/i18n/locales/no.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const no: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Kobler til igjen…',
|
|
21
21
|
stalled: 'Svaret lages fortsatt.',
|
|
22
22
|
retry: 'Prøv igjen',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Ny chat',
|
|
24
|
+
sessions: 'Tidligere chatter',
|
|
25
|
+
sessionsEmpty: 'Ingen tidligere chatter ennå',
|
|
26
26
|
loadError: 'Samtalen kunne ikke lastes.',
|
|
27
27
|
helpLabel: 'Hjelp',
|
|
28
28
|
helpDictate: 'Start eller stopp diktering',
|
package/src/i18n/locales/pl.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const pl: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Łączę ponownie…',
|
|
21
21
|
stalled: 'Odpowiedź wciąż powstaje.',
|
|
22
22
|
retry: 'Spróbuj ponownie',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Nowy czat',
|
|
24
|
+
sessions: 'Wcześniejsze czaty',
|
|
25
|
+
sessionsEmpty: 'Brak wcześniejszych czatów',
|
|
26
26
|
loadError: 'Nie udało się wczytać rozmowy.',
|
|
27
27
|
helpLabel: 'Pomoc',
|
|
28
28
|
helpDictate: 'Włącz lub zatrzymaj dyktowanie',
|
|
@@ -20,9 +20,9 @@ export const ptBR: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Reconectando…',
|
|
21
21
|
stalled: 'A resposta ainda está sendo preparada.',
|
|
22
22
|
retry: 'Tentar de novo',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Novo chat',
|
|
24
|
+
sessions: 'Chats anteriores',
|
|
25
|
+
sessionsEmpty: 'Ainda não há chats anteriores',
|
|
26
26
|
loadError: 'Não foi possível carregar a conversa.',
|
|
27
27
|
helpLabel: 'Ajuda',
|
|
28
28
|
helpDictate: 'Iniciar ou parar o ditado',
|
package/src/i18n/locales/ru.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const ru: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Подключаемся снова…',
|
|
21
21
|
stalled: 'Ответ ещё готовится.',
|
|
22
22
|
retry: 'Повторить',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Новый чат',
|
|
24
|
+
sessions: 'Прошлые чаты',
|
|
25
|
+
sessionsEmpty: 'Прошлых чатов пока нет',
|
|
26
26
|
loadError: 'Не удалось загрузить разговор.',
|
|
27
27
|
helpLabel: 'Справка',
|
|
28
28
|
helpDictate: 'Начать или остановить диктовку',
|
package/src/i18n/locales/sv.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const sv: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Återansluter…',
|
|
21
21
|
stalled: 'Svaret är fortfarande på väg.',
|
|
22
22
|
retry: 'Försök igen',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Ny chatt',
|
|
24
|
+
sessions: 'Tidigare chattar',
|
|
25
|
+
sessionsEmpty: 'Inga tidigare chattar ännu',
|
|
26
26
|
loadError: 'Konversationen kunde inte laddas.',
|
|
27
27
|
helpLabel: 'Hjälp',
|
|
28
28
|
helpDictate: 'Starta eller stoppa diktering',
|
package/src/i18n/locales/tr.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const tr: AssistantCopy = {
|
|
|
20
20
|
reconnecting: 'Yeniden bağlanıyor…',
|
|
21
21
|
stalled: 'Yanıt hâlâ hazırlanıyor.',
|
|
22
22
|
retry: 'Tekrar dene',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: 'Yeni sohbet',
|
|
24
|
+
sessions: 'Önceki sohbetler',
|
|
25
|
+
sessionsEmpty: 'Henüz önceki sohbet yok',
|
|
26
26
|
loadError: 'Konuşma yüklenemedi.',
|
|
27
27
|
helpLabel: 'Yardım',
|
|
28
28
|
helpDictate: 'Dikteyi başlat veya durdur',
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -20,9 +20,9 @@ export const zh: AssistantCopy = {
|
|
|
20
20
|
reconnecting: '正在重新连接…',
|
|
21
21
|
stalled: '仍在生成回答。',
|
|
22
22
|
retry: '重试',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
newChat: '新对话',
|
|
24
|
+
sessions: '以前的对话',
|
|
25
|
+
sessionsEmpty: '还没有以前的对话',
|
|
26
26
|
loadError: '无法加载对话。',
|
|
27
27
|
helpLabel: '帮助',
|
|
28
28
|
helpDictate: '开始或停止听写',
|
package/src/i18n/types.ts
CHANGED
|
@@ -26,10 +26,10 @@ export interface AssistantCopy {
|
|
|
26
26
|
reconnecting: string;
|
|
27
27
|
stalled: string;
|
|
28
28
|
retry: string;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/** Sessions: start a new chat, the list of earlier ones, and its empty line. */
|
|
30
|
+
newChat: string;
|
|
31
|
+
sessions: string;
|
|
32
|
+
sessionsEmpty: string;
|
|
33
33
|
loadError: string;
|
|
34
34
|
/** The "Help" link under the composer and its list of keys. */
|
|
35
35
|
helpLabel: string;
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
export { AssistantChat, type DataPartRenderer } from './AssistantChat';
|
|
2
2
|
export { scriptedTransport, type ScriptStep } from './stream/scriptedTransport';
|
|
3
|
-
export { useAssistantCopy, ASSISTANT_DICTIONARIES, type AssistantCopy } from './i18n';
|
|
3
|
+
export { useAssistantCopy, useAssistantLocale, ASSISTANT_DICTIONARIES, type AssistantCopy } from './i18n';
|
|
4
|
+
export { askAssistant } from './composer/handoff';
|
|
4
5
|
export { ChatDock } from './shell/ChatDock';
|
|
5
6
|
export { AssistantFrame, type FrameLabels } from './shell/AssistantFrame';
|
|
6
7
|
export { AssistantFab, type FabHint } from './shell/AssistantFab';
|
|
7
8
|
export { AssistantLocked } from './shell/AssistantLocked';
|
|
8
9
|
export { createAssistantUi, useMediaQuery, type UiOptions, type UiState, type UiStorage } from './shell/panelState';
|
|
9
10
|
export { AssistantHostProvider, AssistantPortal, AssistantSlot } from './Host';
|
|
10
|
-
export { useSession, type Session, type SessionCopy, type SessionDeps, type SessionNotice, type
|
|
11
|
+
export { useSession, type Session, type SessionCopy, type SessionDeps, type SessionNotice, type ThreadState } from './stream/useSession';
|
|
12
|
+
export { relativeTime, restoreSession, type SessionStorage, type ThreadApi } from './stream/sessions';
|
|
13
|
+
export { SessionsMenu } from './sessions/SessionsMenu';
|
|
11
14
|
export { createStreamAdapter, type AdapterDeps } from './stream/adapter';
|
|
12
15
|
export { reconcile, type Reconciled } from './stream/reconcile';
|
|
13
16
|
export { fromStored, fromStoredThread } from './stream/fromStored';
|
|
14
|
-
export { createTransport, ChatHttpError, ChatNetworkError, type ChatTransport, type RejectCode, type SendInput, type TransportConfig } from './wire/transport';
|
|
17
|
+
export { createTransport, ChatHttpError, ChatNetworkError, ThreadNotFoundError, type ChatTransport, type RejectCode, type SendInput, type TransportConfig } from './wire/transport';
|
|
15
18
|
export { StreamLostError } from './wire/parseSSE';
|
|
16
19
|
export { decodeFrame, WIRE_VERSION } from './wire/frames';
|
|
17
20
|
export type { ChatContext, Frame, Phase, StreamErrorCode } from './wire/frames';
|
|
18
|
-
export type { StoredConversation, StoredMessage, StoredPart, StoredStatus } from './wire/stored';
|
|
21
|
+
export type { StoredConversation, StoredMessage, StoredPart, StoredStatus, StoredThread } from './wire/stored';
|
|
19
22
|
export { citationsRenderer, KB_RENDERERS, KB_PAYLOAD_KINDS, type Citation } from './examples/KbAssistant';
|