@djangocfg/widget-assistant 0.3.7 → 0.3.9
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 +5 -1
- package/package.json +7 -7
- package/src/Host.tsx +31 -2
- package/src/index.ts +1 -1
- package/src/shell/AssistantFrame.tsx +3 -2
- package/src/shell/ChatDock.tsx +5 -2
- package/src/styles/widget.css +13 -4
- package/src/wire/fixtures/events.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,10 @@ It knows nothing about one site (`src/boundary.test.ts` enforces it), so what a
|
|
|
7
7
|
<AssistantChat adapter={adapter} suggestions={suggestions} dataRenderers={renderers} />
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
## Docs
|
|
11
|
+
|
|
12
|
+
Longer pages live in [`@docs/`](./@docs/README.md) (repository only, not in the npm package): architecture, the wire contract, the Python server half, connecting a new project, customization, troubleshooting. The server half is not in django-cfg's config: each product writes its own on the agent plane (`@docs/server.md`).
|
|
13
|
+
|
|
10
14
|
## Folders (one job each)
|
|
11
15
|
|
|
12
16
|
| Folder | Job |
|
|
@@ -69,7 +73,7 @@ Dictation start/stop tones are bundled (Web Audio, no files). `sounds={false}` s
|
|
|
69
73
|
## Asking from outside, the dock and the locked room
|
|
70
74
|
|
|
71
75
|
* `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.
|
|
76
|
+
* `<AssistantHeaderActions><SessionsMenu session={session} /></AssistantHeaderActions>`, rendered beside `AssistantPortal`, publishes the header controls; `AssistantFrame` and `ChatDock` show them with no wiring between the files (an explicit `actions` prop wins). `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
77
|
* `AssistantLocked` takes `onComposerClick`: the inert composer becomes a button for the way out (sign in).
|
|
74
78
|
|
|
75
79
|
## Tests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/widget-assistant",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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.25",
|
|
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.624",
|
|
52
|
+
"@djangocfg/ui-core": "^2.1.624",
|
|
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.624",
|
|
58
|
+
"@djangocfg/typescript-config": "^2.1.624",
|
|
59
|
+
"@djangocfg/ui-core": "^2.1.624",
|
|
60
60
|
"@storybook/react-vite": "^10.6.5",
|
|
61
61
|
"@types/node": "^25.9.5",
|
|
62
62
|
"@types/react": "19.3.0",
|
package/src/Host.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
3
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -13,6 +13,9 @@ interface HostValue {
|
|
|
13
13
|
park: HTMLElement;
|
|
14
14
|
}
|
|
15
15
|
const HostContext = createContext<HostValue | null>(null);
|
|
16
|
+
/** The header controls the conversation publishes (`AssistantHeaderActions`), and the stable setter it publishes them with. Two contexts: the publisher must not re-render when they change. */
|
|
17
|
+
const ActionsContext = createContext<ReactNode>(null);
|
|
18
|
+
const SetActionsContext = createContext<((actions: ReactNode) => void) | null>(null);
|
|
16
19
|
|
|
17
20
|
export function AssistantHostProvider({ children }: { children: ReactNode }) {
|
|
18
21
|
// Browser only: on the server there is no element, and slots render empty.
|
|
@@ -32,7 +35,14 @@ export function AssistantHostProvider({ children }: { children: ReactNode }) {
|
|
|
32
35
|
return () => value.park.remove();
|
|
33
36
|
}, [value]);
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
const [actions, setActions] = useState<ReactNode>(null);
|
|
39
|
+
return (
|
|
40
|
+
<HostContext.Provider value={value}>
|
|
41
|
+
<SetActionsContext.Provider value={setActions}>
|
|
42
|
+
<ActionsContext.Provider value={actions}>{children}</ActionsContext.Provider>
|
|
43
|
+
</SetActionsContext.Provider>
|
|
44
|
+
</HostContext.Provider>
|
|
45
|
+
);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/** Renders the chat (`children`) into the host element. Mount it once, for a signed-in viewer, inside the provider. */
|
|
@@ -55,3 +65,22 @@ export function AssistantSlot({ className }: { className?: string }) {
|
|
|
55
65
|
}, [host]);
|
|
56
66
|
return <div ref={box} className={className} />;
|
|
57
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Publishes the header controls (`<SessionsMenu session={session} />`) from wherever the conversation is mounted. `AssistantFrame` and `ChatDock` show them next to Close
|
|
71
|
+
* without the host passing them through its component tree: the column, the dock and the conversation are usually three different files. Render it beside `AssistantPortal`.
|
|
72
|
+
* An explicit `actions` prop on the frame or the dock still wins.
|
|
73
|
+
*/
|
|
74
|
+
export function AssistantHeaderActions({ children }: { children: ReactNode }) {
|
|
75
|
+
const set = useContext(SetActionsContext);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
set?.(children);
|
|
78
|
+
}, [set, children]);
|
|
79
|
+
useEffect(() => () => set?.(null), [set]);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The controls published by `AssistantHeaderActions`, or null. */
|
|
84
|
+
export function useHeaderActions(): ReactNode {
|
|
85
|
+
return useContext(ActionsContext);
|
|
86
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { AssistantFrame, type FrameLabels } from './shell/AssistantFrame';
|
|
|
7
7
|
export { AssistantFab, type FabHint } from './shell/AssistantFab';
|
|
8
8
|
export { AssistantLocked } from './shell/AssistantLocked';
|
|
9
9
|
export { createAssistantUi, useMediaQuery, type UiOptions, type UiState, type UiStorage } from './shell/panelState';
|
|
10
|
-
export { AssistantHostProvider, AssistantPortal, AssistantSlot } from './Host';
|
|
10
|
+
export { AssistantHostProvider, AssistantPortal, AssistantSlot, AssistantHeaderActions } from './Host';
|
|
11
11
|
export { useSession, type Session, type SessionCopy, type SessionDeps, type SessionNotice, type ThreadState } from './stream/useSession';
|
|
12
12
|
export { relativeTime, restoreSession, type SessionStorage, type ThreadApi } from './stream/sessions';
|
|
13
13
|
export { SessionsMenu } from './sessions/SessionsMenu';
|
|
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState, type CSSProperties, type Keyb
|
|
|
4
4
|
|
|
5
5
|
import { ChatPanelHeader, PANEL_TITLE_CLASS, PanelCloseButton } from '@djangocfg/widget-chat';
|
|
6
6
|
|
|
7
|
-
import { AssistantSlot } from '../Host';
|
|
7
|
+
import { AssistantSlot, useHeaderActions } from '../Host';
|
|
8
8
|
import { useAssistantCopy } from '../i18n';
|
|
9
9
|
|
|
10
10
|
export interface FrameLabels {
|
|
@@ -97,6 +97,7 @@ export function AssistantFrame({ docked, width, minWidth, maxWidth, defaultWidth
|
|
|
97
97
|
|
|
98
98
|
function Column({ width, shown, minWidth, maxWidth, defaultWidth, onWidthChange, onClose, labels, content, actions }: Omit<Props, 'docked' | 'children'> & { shown: boolean }) {
|
|
99
99
|
const copy = useAssistantCopy();
|
|
100
|
+
const published = useHeaderActions();
|
|
100
101
|
// The handle sits on the column's left edge: the pointer's distance from the window's right edge is the new width.
|
|
101
102
|
const drag = useCallback(
|
|
102
103
|
(event: PointerEvent<HTMLDivElement>) => {
|
|
@@ -142,7 +143,7 @@ function Column({ width, shown, minWidth, maxWidth, defaultWidth, onWidthChange,
|
|
|
142
143
|
/>
|
|
143
144
|
<ChatPanelHeader
|
|
144
145
|
heading={<h2 className={PANEL_TITLE_CLASS}>{labels.title}</h2>}
|
|
145
|
-
actions={actions}
|
|
146
|
+
actions={actions ?? published}
|
|
146
147
|
close={<PanelCloseButton label={labels.close ?? copy.close} onClick={onClose} />}
|
|
147
148
|
/>
|
|
148
149
|
{content ? <div className="flex min-h-0 flex-1 flex-col">{content}</div> : <AssistantSlot className="min-h-0 flex-1" />}
|
package/src/shell/ChatDock.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { SidePanel } from '@djangocfg/ui-core/components';
|
|
|
6
6
|
import { useIsMobile } from '@djangocfg/ui-core/hooks';
|
|
7
7
|
import { ChatPanelHeader, ChatSheet, PANEL_TITLE_CLASS, PanelCloseButton } from '@djangocfg/widget-chat';
|
|
8
8
|
|
|
9
|
+
import { useHeaderActions } from '../Host';
|
|
9
10
|
import { useAssistantCopy } from '../i18n';
|
|
10
11
|
|
|
11
12
|
interface Props {
|
|
@@ -26,10 +27,12 @@ interface Props {
|
|
|
26
27
|
export function ChatDock({ open, onOpenChange, children: chat, title, contextLine, closeLabel, actions }: Props) {
|
|
27
28
|
const isMobile = useIsMobile();
|
|
28
29
|
const copy = useAssistantCopy();
|
|
30
|
+
const published = useHeaderActions();
|
|
31
|
+
const shown = actions ?? published;
|
|
29
32
|
|
|
30
33
|
if (isMobile) {
|
|
31
34
|
return (
|
|
32
|
-
<ChatSheet open={open} onOpenChange={onOpenChange} title={title} description={contextLine} closeLabel={closeLabel ?? copy.close} actions={
|
|
35
|
+
<ChatSheet open={open} onOpenChange={onOpenChange} title={title} description={contextLine} closeLabel={closeLabel ?? copy.close} actions={shown}>
|
|
33
36
|
{chat}
|
|
34
37
|
</ChatSheet>
|
|
35
38
|
);
|
|
@@ -41,7 +44,7 @@ export function ChatDock({ open, onOpenChange, children: chat, title, contextLin
|
|
|
41
44
|
<ChatPanelHeader
|
|
42
45
|
heading={<SidePanel.Title className={PANEL_TITLE_CLASS}>{title}</SidePanel.Title>}
|
|
43
46
|
description={contextLine ? <SidePanel.Description>{contextLine}</SidePanel.Description> : undefined}
|
|
44
|
-
actions={
|
|
47
|
+
actions={shown}
|
|
45
48
|
close={<PanelCloseButton label={closeLabel ?? copy.close} onClick={() => onOpenChange(false)} />}
|
|
46
49
|
/>
|
|
47
50
|
<SidePanel.Body>{chat}</SidePanel.Body>
|
package/src/styles/widget.css
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/* The widget's own stylesheet carries the visual rules (composer, transcript, markdown); it must join the Tailwind pipeline by @import, not by a JS import in a component. */
|
|
2
2
|
@import "@djangocfg/widget-chat/styles";
|
|
3
3
|
|
|
4
|
-
/* Tailwind must see the widgets' classes: without these sources it purges them silently
|
|
5
|
-
@source
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/* Tailwind must see the widgets' classes: without these sources it purges them silently.
|
|
5
|
+
An @source path is relative to THIS file, and the file sits at <pkg>/src/styles, so "../../../" is the folder that holds the package:
|
|
6
|
+
- installed (npm, or pnpm with hoisting): node_modules/@djangocfg/, where the packages are named widget-chat, widget-kit, widget-media;
|
|
7
|
+
- the django-cfg monorepo: frontend/widgets/, where the same packages are the folders chat, kit, media.
|
|
8
|
+
A path that matches nothing in one layout is harmless, so both are listed. Checked with Tailwind 4.3 against a flat install, a pnpm
|
|
9
|
+
install and the monorepo (see @docs/troubleshooting.md). A strict pnpm install without hoisting cannot reach widget-kit and widget-media
|
|
10
|
+
from here: list them as direct dependencies of the app, or name them in the app's own @source (see @docs/connect.md). */
|
|
11
|
+
@source "../../../widget-chat/src/**/*.{ts,tsx}";
|
|
12
|
+
@source "../../../widget-kit/src/**/*.{ts,tsx}";
|
|
13
|
+
@source "../../../widget-media/src/**/*.{ts,tsx}";
|
|
14
|
+
@source "../../../chat/src/**/*.{ts,tsx}";
|
|
15
|
+
@source "../../../kit/src/**/*.{ts,tsx}";
|
|
16
|
+
@source "../../../media/src/**/*.{ts,tsx}";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_note": "One of each frame the server speaks. The Python
|
|
2
|
+
"_note": "One of each frame the server speaks. Canonical copy: this file. The Python copy is django_cfg/modules/django_llm/agent/bus/tests/fixtures/events.json; bus/tests/test_wire_contract.py fails when the two differ or when a bus event no longer encodes to a frame here, and wire.test.ts decodes every frame here. Change both files in one commit.",
|
|
3
3
|
"frames": [
|
|
4
4
|
{ "type": "run", "version": 1, "thread_id": "5f0c8a52-7f0e-4c43-9a43-0e3b5a0d2f11", "message_id": "9b1e2c7a-4a0f-4d52-8d0d-3f1f6c9a7e22", "client_id": "c0a8e1d2-8f2e-4b7e-a1c4-6f5d8c1b2a33", "seq": 2 },
|
|
5
5
|
{ "type": "status", "phase": "thinking" },
|