@djangocfg/widget-assistant

中文文档
0.3.24发布于 2026/11/09MITTypeScriptES Module近 30 天下载 5568

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

原始使用文档
文档目录11 节
  1. Docs
  2. Folders (one job each)
  3. Behaviour worth knowing
  4. The wire (0.2)
  5. Sessions (0.3)
  6. Voice (0.3.3)
  7. Attachments
  8. Asking from outside, the dock and the locked room
  9. Tests
  10. Migrating from 0.1 (a host that spoke the film frames)
  11. Migrating from 0.2 (one thread, clear() )

@djangocfg/widget-assistant

The assistant chat for a site: the SSE wire and session, the persistent host, the docked column, the floating button and the phone sheet, on top of @djangocfg/widget-chat. It knows nothing about one site (src/boundary.test.ts enforces it), so what a host passes stays small: a language (from the framework's locale provider), an adapter, suggestions and its own part renderers. Source-only: the host transpiles it (DEFAULT_TRANSPILE_PACKAGES) and imports @djangocfg/widget-assistant/styles once.

<AssistantChat adapter={adapter} suggestions={suggestions} dataRenderers={renderers} />

Docs

Longer pages live in @docs/ (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).

Folders (one job each)

FolderJob
wire/the HTTP and SSE contract: frames, parser, transport, stored conversation
stream/one conversation: adapter, session hook, reconcile after a lost stream, the scripted transport for stories
composer/what the widget's composer lacks, mounted inside it (ComposerBehaviour): dictation keys, caret, height, the Help list
messages/the rows around messages: copy footer, the reconnecting/stalled line
shell/where the chat lives: column frame, dock/sheet, floating button with its hint, locked view, open/width state
i18n/the assistant's own words, 17 languages bundled (locales/), useAssistantCopy(); the language comes from @djangocfg/i18n's provider, the same one the widget's own copy follows
styles/widget stylesheet entry, layout, composer and button motion

Host.tsx (exported as ./host) keeps one mounted chat for many surfaces; AssistantChat.tsx composes the widget.

Behaviour worth knowing

  • Dictation keys. ⌘D / Ctrl+D starts and stops (formatHotkey draws the right one for the system). While dictating, Enter stops and sends, Escape stops and keeps the text, from anywhere. Every stop waits with the microphone open until the field stops changing (450 ms of quiet, 2.5 s at most): the engine reports words late, and the widget's stop closes it and drops them. The engine is our own (composer/wholeTranscript.ts): it rebuilds the whole transcript from the browser's results at every event and restarts itself when the browser ends a run, because the widget's stitching of separate results kept only the last words.
  • Field. While dictating its height only grows (no jumping between bursts). Pressing the empty strip around the composer focuses the field (keyboard devices only).
  • Caret. Opening the panel on a keyboard device focuses the field; after a send the caret returns to the field, or to the microphone when the send ended a dictation (Enter then starts the next one). Never on phones.
  • Floating button. Pops in, shows a message-style hint after a moment (hint prop; text defaults to the bundled one), then a ring every so often. No close control; the host decides how often to show it.
  • Help. A round ? in the panel header (AssistantHelp, shown by the frame and the dock) opens the list of keys in a popover; hidden on touch devices.
  • Time. UTC on the wire, the reader's zone and locale on screen. The time and copy row under a message appears on hover; the newest message keeps it in view, but not while its answer is still streaming and never for an empty row.

The wire (0.2)

One vocabulary, data: {json with type}\n\n, decoded only in wire/frames.ts:

FrameFields
runversion, thread_id, message_id, client_id, seq
statusphase: thinking | searching | answering
text_deltadelta
reasoning_deltadelta (optional layer: the model's thinking, drawn by the widget's own Reasoning block)
reasoning_endelapsed_ms?, tokens? (optional: seals the thought; otherwise the next text, payload or done does)
ui_payloadkind, data: the host draws it through dataRenderers[kind]; list the kinds in payloadKinds and any other is dropped and reported to onUnknownPayload
donetokens_in, tokens_out, cost_usd, finish_reason, replayed?
errorcode (unavailable | timeout | failed), message, retryable

An unknown type is ignored. wire/fixtures/events.json holds one of each frame and is committed on the server side too: drift fails a test on either side. A public chat must not forward tool_call / tool_result.

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.

Sessions (0.3)

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).

  • 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).
  • 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.
  • createTransport({ streamUrl: ({ threadId }) => … }) posts to the session in the path; SendInput.threadId is what threadFor returned.
  • The history is a screen of its own, drawn over the conversation (Messages, Notes), not a dropdown that grows with the list: Today / Yesterday / one heading per month, a search field from seven chats, the open chat marked, a click opens it and returns. Mount <AssistantHistory session={session} /> beside AssistantPortal (inside AssistantHostProvider) and open it from any control with useAssistantHistory().setOpen(true); <SessionsMenu session={session} /> is the ready-made pair of header buttons (New chat, History / Back to the chat). A host with its own "..." menu (UI Core MenuBuilder, passed as SessionsMenu's children or in actions) keeps "New chat" there and replaces the list of chats with one "History" row calling setOpen(true). The chat stays mounted under the screen, so a run keeps streaming. Words: newChat, sessions, sessionsEmpty, sessionsSearch, sessionsNoMatch, sessionsBack; the headings come from Intl.
  • <AssistantChat draftRoom={session.thread.session ?? 'new'} /> keeps each session's unsent draft under its own id.
  • There is no delete and no clear(): "clear" is "new chat". Retention is the server's job.
  • Without api (anonymous host) nothing is listed; startNew() just empties the room.

Voice (0.3.3)

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 owns it through useSpeechLanguage from @djangocfg/ui-core/lib/speech-language (the last pick, shared by every chat and tab on this device under cfg.chat.speech_language; else the first of the browser's languages dictation supports, the page language only breaking a tie; else English) and feeds createWholeTranscriptSpeech. A host speech={{ value, onChange }} still overrides it (controlled mode). The picker lists the base widget's Web Speech catalogue (61 languages, with dialects, searchable), which covers all 17 bundled locales (zh is Mandarin, no is Bokmål); it has no free "other BCP-47" field. Touch devices hide the picker and the microphone (they have their own keyboard dictation). A host that shows no picker runs an older release: upgrade to 0.3.3 or later.

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.

Dictation start/stop tones are bundled (Web Audio, no files). sounds={false} silences them; sounds={{ play(kind) }} replaces them.

Attachments

<AssistantChat attachments={{ upload, kinds, maxFileSize, maxFiles }} /> adds a paperclip, drop and image paste (defaults: image/audio/text/document, 10 MB, 5 files). The types are the chat's, re-exported from this package. upload is the host's; the default wire body gains attachments only when files were attached. Details: @djangocfg/widget-chat @docs/attachments.md.

Asking from outside, the dock and the locked room

  • 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.
  • <AssistantHeaderActions><ChatMenu 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 the "..." menu): 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 (AssistantHelp) is hidden on touch devices.
  • One placement rule, in the frame. <AssistantFrame docked={open} />: from 1280 px the chat is a column beside the page; narrower, the frame itself opens it as a floating window bottom right (tablet; no backdrop, not modal) or a full-screen sheet (phone). A host renders no ChatDock, no media query and no second surface.
  • A host with its own shell or transport (a call that polls its own lines): AssistantFrame heading, help, tablet, windowBottom, stageMin, className, fill; AssistantChat store, capabilities, renderers, footer, composer, readOnly; the entry @djangocfg/widget-assistant/shell leaves the SSE layer out of the graph. All optional; see @docs/interface.md.
  • One signed-out view, in the frame. <AssistantFrame requireAuth lock={...} /> (also on ChatDock): a signed-in reader gets the chat, a reader the client is still identifying gets an empty body, anyone else gets a big icon, a reason and a Sign in button, centred in the full height of the column (no inert composer). Sign in saves the current page and goes to the django-cfg auth route (useAuth from @djangocfg/api/auth, a peer dependency); lock (AssistantLockOptions: title, body, signInLabel, icon, onSignIn, content) changes the words or replaces the block. Also exported: AssistantAuthGate, useAssistantAccess, useSignIn. A host writes no locked wrapper.
  • AssistantLocked (the bare view; its composerLabel and onComposerClick are deprecated and ignored) is what the gate draws; use it directly only on a surface with no django-cfg auth.

Tests

pnpm --filter @djangocfg/widget-assistant check test: the wire, stream and boundary tests, and tsc --noEmit.

Migrating from 0.1 (a host that spoke the film frames)

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.

Migrating from 0.2 (one thread, clear())

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.

版本与发布记录

@djangocfg/widget-assistant 共发布 29 个版本,首次发布于 2026/10/25;过去 12 个月发布了 29 个版本。

版本发布日期距今
0.3.24latest2026/11/092 天前
0.3.232026/11/092 天前
0.3.222026/11/093 天前
0.3.212026/11/083 天前
0.3.202026/11/083 天前
0.3.192026/11/084 天前

关于 @djangocfg/widget-assistant 的常见问题

@djangocfg/widget-assistant 是什么?

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

如何安装 @djangocfg/widget-assistant?

在项目目录运行 npm install @djangocfg/widget-assistant,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.0.0)、lucide-react(^1.68.0)、@djangocfg/api(^2.1.644)、@djangocfg/i18n(^2.1.644)、@djangocfg/ui-core(^2.1.644)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@djangocfg/widget-assistant 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@djangocfg/widget-assistant 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@djangocfg/widget-assistant 可以商用吗?

@djangocfg/widget-assistant 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@djangocfg/widget-assistant 的下载量有多少?

根据 npm 官方下载统计,@djangocfg/widget-assistant 在 2026/10/13 – 2026/11/11 这 30 天内被下载 5,568 次,日均约 186 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@djangocfg/widget-assistant 还在维护吗?

最新版本 0.3.24 发布于 2026/11/09(2 天前);至今共发布 29 个版本,其中过去 12 个月发布了 29 个;首次发布于 2026/10/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。