@4djs/assistant
v0.1.12
Published
Embeddable React chat assistant with LLM tool-calling, streaming, markdown rendering, and an in-app LLM settings panel. Connect any OpenAI-compatible provider — cloud APIs or a local server such as Ollama or LM Studio.
Readme
@4djs/assistant
Embeddable React chat assistant with LLM tool-calling, streaming, markdown rendering, and an in-app LLM settings panel. Connect any OpenAI-compatible provider — cloud APIs or a local server such as Ollama or LM Studio.
Install
npm install @4djs/assistant
# or
bun add @4djs/assistantPeer dependencies: react and react-dom (^18 or ^19). React is not bundled — your app supplies it.
Import styles once in your app entry (required for the UI):
import "@4djs/assistant/styles.css";The stylesheet uses CSS variables from your host app (--brand, --surface-panel, --text-heading, etc.). Define those tokens in your global CSS so the assistant matches your design system.
Quick start
The simplest integration is AssistantRoot, which wires the provider, store bootstrap, and default panel UI:
import { AssistantRoot, type AssistantConfig } from "@4djs/assistant";
const config: AssistantConfig = {
llm: {
enabled: true,
baseUrl: import.meta.env.VITE_LLM_BASE_URL ?? "https://api.openai.com/v1",
apiKey: import.meta.env.VITE_LLM_KEY ?? null,
model: import.meta.env.VITE_LLM_MODEL ?? "gpt-4o-mini",
systemPrompt: "You are a helpful assistant with access to tools.",
},
storageKeys: {
history: "my-app-chat-history",
llmSettings: "my-app-llm-settings",
},
welcomeMessage: ({ llmEnabled, model }) => ({
id: "welcome",
role: "assistant",
content: llmEnabled
? `Hello! Using **${model ?? "LLM"}**. Ask me anything.`
: "Hello! Connect an LLM in **LLM settings** to start chatting.",
timestamp: Date.now(),
}),
listTools: async () => myTools,
invokeTool: async (name, args) => myToolRunner(name, args),
};
export function MyAssistant() {
return <AssistantRoot config={config} />;
}When no LLM is configured, chat input is disabled and the UI prompts users to open LLM settings. Settings and clear-chat remain available without a provider.
Configuration
AssistantConfig extends the store dependencies with UI options.
Required store hooks
Provide toolRegistry (recommended) or both listTools and invokeTool:
| Option | Description |
| --- | --- |
| welcomeMessage(ctx) | Returns the initial assistant message. Called again when LLM status or model changes. |
| toolRegistry | AssistantToolRegistry from createAssistantToolRegistry() — register, deactivate, and invoke tools in one place. |
| listTools() | Returns OpenAI-style tool definitions (name, description, inputSchema). |
| invokeTool(name, args) | Executes a tool and returns { content, isError?, structuredContent? }. |
Tool registry (@4djs/assistant/core and @4djs/assistant/tools)
import {
connectExternalTools,
createAssistantToolRegistry,
} from "@4djs/assistant/tools";
import { registerDatastoreTools } from "@4djs/assistant/tools";
const registry = createAssistantToolRegistry();
registerDatastoreTools(registry, myDatastoreAdapter);
// Extend at runtime
registry.register({
definition: { name: "my_tool", description: "…", inputSchema: { type: "object", properties: {} } },
invoke: async (args) => ({ content: [{ type: "text", text: "ok" }] }),
});
registry.deactivate("my_tool"); // hide from LLM without unregistering
registry.activate("my_tool");
// Bridge @4d/tools or any external registry
connectExternalTools(registry, externalRegistry);
const config: AssistantConfig = {
toolRegistry: registry,
// …
};Optional store hooks
| Option | Description |
| --- | --- |
| llm | OpenAI-compatible provider settings (see below). |
| storageKeys | localStorage keys for chat history and LLM settings (including selected model). |
| onToolInvoked | Side-effect hook after each tool completes (e.g. refresh app state). |
| fetchSuggestedPrompts | Async hook to generate contextual starter prompts via LLM. |
| autoLoadLlmStatus | Fetch LLM config on mount (default: true). |
| labels | Partial UI copy overrides for translation or branding (see below). |
UI options
{
header: {
title: "Assistant",
subtitle: "Query and explore your data",
icon: Sparkles, // lucide-react icon
showClearButton: true, // footer toolbar (default: true)
showSuggestionsButton: true, // footer toolbar when fetchSuggestedPrompts is set
},
emptyState: {
title: "Get started",
description: "Ask a question or pick a suggestion below.",
dynamicSuggestedPrompts: true, // manual LLM fetch on welcome screen
suggestedPrompts: [ // static fallback when LLM is off
{ id: "catalog", label: "Show catalog", prompt: "catalog", icon: Database },
],
},
ui: {
composerPlaceholder: "Ask the assistant…",
showModelSelector: true,
maxWidth: "56rem",
className: "panel", // extra class on the root panel
},
}Per-instance overrides are supported via Assistant / AssistantRoot props: header, emptyState, ui.
Labels and i18n
All built-in UI strings (buttons, placeholders, error titles, trace labels, aria text, etc.) live in a flat dictionary keyed by dot notation, e.g. composer.placeholder, commands.clear.description, errors.network.title.
Pass partial overrides on AssistantConfig.labels. Only the keys you set are replaced; everything else keeps the English default:
import {
AssistantRoot,
DEFAULT_ASSISTANT_LABELS,
type AssistantConfig,
} from "@4djs/assistant";
const config: AssistantConfig = {
// …store hooks
labels: {
"header.title": "Assistant",
"composer.placeholder": "Posez une question…",
"composer.placeholderDisabled":
"LLM non configuré — le chat est désactivé",
"common.save": "Enregistrer",
"common.cancel": "Annuler",
"llmSettings.title": "Paramètres LLM",
"errors.network.title": "Connexion perdue",
"commands.clear.description": "Effacer la conversation",
},
};For a full locale file, spread it in:
import { frLabels } from "./locales/fr";
labels: { ...frLabels, "header.title": "Mon assistant" },Some keys support placeholders — use {name} or {model} in the string; the package substitutes them at runtime:
labels: {
"activity.steps.running": "Exécution de {name}…",
"llmSettings.connected": "Connecté · {model}",
"commands.unknown": "Commande inconnue : /{name}",
},Use formatLabel from @4djs/assistant (or @4djs/assistant/core) if you need the same substitution in host code.
Key groups
| Prefix | Examples |
| --- | --- |
| common.* | save, cancel, loading, tryAgain |
| composer.* | placeholder, send, stop, hint |
| llmSettings.* | Form labels, placeholders, connected |
| llmSetup.* | “Connect an LLM” banner copy |
| suggestions.* / emptyState.* | Prompt strips and welcome CTA |
| activity.* | Trace panel and tool step labels |
| interactive.* | Confirmation / choice prompts |
| errors.* | Parsed error titles and hints |
| commands.* | /clear description and errors |
The full list of keys is ASSISTANT_LABEL_KEYS, or inspect DEFAULT_ASSISTANT_LABELS.
Legacy shortcuts
These older options still work and are merged into labels automatically:
header.title→labels["header.title"]ui.composerPlaceholder→labels["composer.placeholder"]
Prefer labels for new work so all copy stays in one place.
Custom components
Inside AssistantProvider, read the resolved dictionary with useAssistantLabels():
import { useAssistantLabels } from "@4djs/assistant";
function MyPanel() {
const labels = useAssistantLabels();
return <p>{labels["common.loading"]}</p>;
}Outside the provider, components fall back to DEFAULT_ASSISTANT_LABELS.
Headless helpers (also exported from @4djs/assistant/core):
| Export | Use |
| --- | --- |
| DEFAULT_ASSISTANT_LABELS | English defaults |
| ASSISTANT_LABEL_KEYS | All valid keys |
| resolveAssistantLabels(overrides?) | Merge overrides onto defaults |
| formatLabel(template, values) | Replace {placeholders} |
Not covered by labels: welcomeMessage content, emptyState.title / description, and static or dynamic suggestedPrompts (label, hint, prompt) — those remain app-defined on AssistantConfig.
LLM provider
The assistant calls an OpenAI-compatible chat completions API directly from the browser (or your host runtime). No proxy /api/chat routes are required.
Pass llm on AssistantConfig:
import type { AssistantLlmSettings } from "@4djs/assistant/core";
const llm: AssistantLlmSettings = {
enabled: true,
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.LLM_KEY ?? null,
model: "gpt-4o-mini",
systemPrompt: "You are a helpful assistant with access to tools.",
models: ["gpt-4o-mini", "gpt-4o"], // optional static list
};| Field | Description |
| --- | --- |
| enabled | When false, chat is disabled until the user configures a provider. |
| baseUrl | Provider base URL (e.g. https://api.openai.com/v1 or http://127.0.0.1:11434/v1). |
| apiKey | Bearer token for remote providers. Optional for local servers on localhost, 127.0.0.1, or *.local. |
| model | Default model when the user has not picked one. |
| models | Optional static model list. When omitted, models are fetched from {baseUrl}/models. |
| systemPrompt | Prepended system message for each completion. |
You can also pass a resolver function for dynamic config:
llm: async () => ({
enabled: true,
baseUrl: "https://api.openai.com/v1",
apiKey: await getApiKeyFromSecureStorage(),
model: "gpt-4o-mini",
}),Configure globally outside React:
import { configureAssistantLlm } from "@4djs/assistant/core";
configureAssistantLlm({ enabled: true, baseUrl: "…", apiKey: "…", model: "…" });Users can override provider settings in the built-in LLM settings panel. Values are persisted under storageKeys.llmSettings (default: assistant-llm-settings).
Environment variables
A typical Vite or Next.js app might expose provider defaults like this:
| Variable | Description |
| --- | --- |
| VITE_LLM_KEY / LLM_KEY | API key for remote providers |
| VITE_LLM_BASE_URL / LLM_BASE_URL | Provider URL (default: OpenAI) |
| VITE_LLM_MODEL / LLM_MODEL | Default model |
| VITE_LLM_MODELS / LLM_MODELS | Comma-separated extra models for the selector |
Security: Calls are made directly to the provider from the client. Any API key in your bundle is visible to users. Use scoped keys for public apps, or proxy requests through your backend for production.
Wiring tools
Tools must match the shape expected by the LLM agent:
import type { AssistantToolDefinition, AssistantToolResult } from "@4djs/assistant";
const tools: AssistantToolDefinition[] = [
{
name: "query_entity",
description: "Query records from a dataclass",
inputSchema: {
type: "object",
properties: {
entity: { type: "string" },
filter: { type: "string" },
},
required: ["entity"],
},
},
];
async function invokeTool(
name: string,
args: Record<string, unknown>,
): Promise<AssistantToolResult> {
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}The store passes listTools output to the LLM on each turn and calls invokeTool for each tool call in the response.
Suggested prompts
Two modes:
- Static — set
emptyState.suggestedPromptswithlabel,hint,prompt, and optional Lucideicon. - Dynamic — implement
fetchSuggestedPrompts. The UI does not auto-fetch; users click Generate suggestions on the welcome screen or the sparkles button in the composer footer.
Dynamic fetch receives { llmEnabled, model, tools } and should return:
Array<{
id: string;
label: string;
description?: string;
prompt: string;
icon?: string; // e.g. "database", "search", "sparkles"
}>Use parseSuggestedPromptsResponse from @4djs/assistant/core to validate LLM JSON output.
Custom composition
Use lower-level exports to build your own layout:
import {
AssistantProvider,
AssistantBootstrap,
Assistant,
useAssistant,
useAssistantActions,
} from "@4djs/assistant";
function CustomShell() {
const messages = useAssistant((s) => s.messages);
const { sendChat } = useAssistantActions();
// render your own chrome around <Assistant /> or individual chat components
}
export function App() {
return (
<AssistantProvider config={config}>
<AssistantBootstrap>
<CustomShell />
</AssistantBootstrap>
</AssistantProvider>
);
}Exported building blocks include ChatComposer, ChatMessageView, ChatEmptyState, ChatActivity, ModelSelector, MarkdownContent, and MermaidDiagram.
Composer commands
The input supports slash commands (e.g. /clear). Extend commands via runAssistantChatCommand and related helpers from @4djs/assistant/core, or handle custom logic before sendChat in your host app.
Core package
Import headless logic without React:
import {
createAssistantStore,
runLlmAgent,
buildLlmHistory,
fetchLlmStatus,
runAssistantChatCommand,
isLlmConfigured,
} from "@4djs/assistant/core";Useful for server routes, tests, or a fully custom UI.
Package exports
| Import | Contents |
| --- | --- |
| @4djs/assistant | React components, hooks, types, labels |
| @4djs/assistant/core | Store, LLM client, commands, interactive tools, labels |
| @4djs/assistant/styles.css | Component styles |
The published package ships compiled JavaScript and TypeScript declarations from dist/. Source is not included in the tarball.
Features
- Streaming LLM responses with tool-call activity timeline
- Interactive tool UI (confirmations, choices, reply suggestions)
- Markdown + GFM + math (KaTeX) + Mermaid diagrams
- Model selector with searchable dropdown
- In-app LLM settings (cloud or local OpenAI-compatible endpoints)
- Persistent chat history and model preference (
localStorage) - Configurable UI labels for translation and branding
- Welcome screen with static or LLM-generated starter prompts
- Composer footer toolbar: model selector, generate suggestions, clear chat
- Structured error states with retry actions
