@twentyfourg/chat-kit 1.0.0-beta.1

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/dist/chat-kit.js +1914 -0
  4. package/dist/chat-kit.js.map +1 -0
  5. package/dist/components/AssistantMessage.vue.d.ts +35 -0
  6. package/dist/components/Chat.vue.d.ts +72 -0
  7. package/dist/components/CitationPopover.vue.d.ts +11 -0
  8. package/dist/components/FeedbackRow.vue.d.ts +11 -0
  9. package/dist/components/IconButton.vue.d.ts +17 -0
  10. package/dist/components/InputBar.vue.d.ts +16 -0
  11. package/dist/components/KitIcon.vue.d.ts +12 -0
  12. package/dist/components/MessageList.vue.d.ts +36 -0
  13. package/dist/components/PageImageModal.vue.d.ts +14 -0
  14. package/dist/components/SourceCard.vue.d.ts +21 -0
  15. package/dist/components/SourceList.vue.d.ts +7 -0
  16. package/dist/components/ThinkingAnimation.vue.d.ts +15 -0
  17. package/dist/components/ThinkingIndicator.vue.d.ts +13 -0
  18. package/dist/components/ThinkingProcess.vue.d.ts +8 -0
  19. package/dist/components/UserMessage.vue.d.ts +7 -0
  20. package/dist/composables/useAutoScroll.d.ts +36 -0
  21. package/dist/composables/useChatEngine.d.ts +335 -0
  22. package/dist/composables/useDictation.d.ts +29 -0
  23. package/dist/copy.d.ts +144 -0
  24. package/dist/index.css +1 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/services/anonymous-auth.d.ts +63 -0
  27. package/dist/services/base-camp-client.d.ts +66 -0
  28. package/dist/services/citations.d.ts +18 -0
  29. package/dist/services/entities.d.ts +29 -0
  30. package/dist/services/markdown.d.ts +47 -0
  31. package/dist/services/math.d.ts +36 -0
  32. package/dist/services/streaming-message.d.ts +32 -0
  33. package/dist/services/streaming.service.d.ts +84 -0
  34. package/dist/services/typewriter.d.ts +54 -0
  35. package/dist/testing/mock-transport.d.ts +52 -0
  36. package/dist/testing/setup-tests.d.ts +8 -0
  37. package/dist/theme.css +122 -0
  38. package/dist/types.d.ts +389 -0
  39. package/package.json +76 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Framework-free Base Camp API client built on fetch. Paths and shapes follow
3
+ * Base Camp's Zodios API definitions (the authoritative OpenAPI contract).
4
+ * No retries and no error swallowing; callers decide how to recover.
5
+ */
6
+ import type { AgentDetails, CreateThreadBody, CreateThreadResponse, FeedbackResponse, MessageListResponse, QuotaResponse, SendMessageBody, StopMessageResponse, Thread, ThreadDetail, TrackEventBody, TrackEventResponse } from '../types';
7
+ export interface BaseCampClientOptions {
8
+ /** Base Camp REST host, without a trailing slash; request paths start with /v1. */
9
+ baseUrl: string;
10
+ /** Streaming host when it differs from the REST host; falls back to baseUrl. */
11
+ streamBaseUrl?: string;
12
+ /** Returns the Authorization credential for the current user. */
13
+ getAuthToken: () => string | Promise<string>;
14
+ /** Value for the x-24g-deploy-env identity header. */
15
+ deployEnv: string;
16
+ /** Value for the x-24g-job-number identity header. */
17
+ jobNumber: string;
18
+ /** Non-prod tenant override sent as x-tenant-hint when provided. */
19
+ tenantHint?: string;
20
+ }
21
+ /** Normalized Base Camp API error. */
22
+ export declare class BaseCampError extends Error {
23
+ readonly status: number;
24
+ /** Machine-readable code when the server provides one, e.g. CONTEXT_LIMIT_EXCEEDED. */
25
+ readonly code?: string;
26
+ constructor(status: number, message: string, code?: string);
27
+ }
28
+ export declare class BaseCampClient {
29
+ private readonly options;
30
+ constructor(options: BaseCampClientOptions);
31
+ /** GET /v1/agents/:agentId (public-safe agent detail; drive feature UI off config). */
32
+ getAgent(agentId: string): Promise<AgentDetails>;
33
+ /**
34
+ * POST /v1/agents/:agentId/threads. The create endpoint only accepts variables;
35
+ * when customData is supplied it is applied with a follow-up PATCH, since the
36
+ * server only reads customData on thread update.
37
+ */
38
+ createThread(agentId: string, body?: CreateThreadBody): Promise<CreateThreadResponse>;
39
+ /** GET /v1/agents/:agentId/threads (current user's threads for the agent). */
40
+ /** GET /v1/agents/:agentId/threads?limit=N. Every status comes back; callers filter. */
41
+ getThreads(agentId: string, limit: number): Promise<Thread[]>;
42
+ /** GET /v1/agents/:agentId/threads/:threadId (includes embedded agent summary and customData). */
43
+ getThread(agentId: string, threadId: string): Promise<ThreadDetail>;
44
+ /** GET .../messages (full history; also the reconnect path after an interrupted stream). */
45
+ getMessages(agentId: string, threadId: string): Promise<MessageListResponse>;
46
+ /**
47
+ * POST .../messages with stream forced on. Returns the raw Response so the
48
+ * streaming service can consume the SSE body; only HTTP-level failures throw here.
49
+ */
50
+ sendMessage(agentId: string, threadId: string, body: SendMessageBody, options?: {
51
+ signal?: AbortSignal;
52
+ }): Promise<Response>;
53
+ /** POST .../stop. Needs the assistant message id captured from the stream's early events. */
54
+ stopMessage(agentId: string, threadId: string, assistantMessageId: string): Promise<StopMessageResponse>;
55
+ /** POST .../messages/:messageId/feedback. Idempotent; resending the same value is a no-op. */
56
+ submitFeedback(agentId: string, threadId: string, messageId: string, feedback: 'POSITIVE' | 'NEGATIVE'): Promise<FeedbackResponse>;
57
+ /** Same endpoint as submitFeedback; omitting the feedback field clears it. */
58
+ clearFeedback(agentId: string, threadId: string, messageId: string): Promise<FeedbackResponse>;
59
+ /** GET /v1/agents/:agentId/quota (read-only; does not consume quota). */
60
+ getQuota(agentId: string): Promise<QuotaResponse>;
61
+ /** POST /v1/analytics/events. Callers usually fire and forget; a lost event is acceptable. */
62
+ trackEvent(body: TrackEventBody): Promise<TrackEventResponse>;
63
+ /** Identity and auth headers Base Camp expects on every request. */
64
+ private identityHeaders;
65
+ private request;
66
+ }
@@ -0,0 +1,18 @@
1
+ import type { ChatCitation } from '../types';
2
+ export interface InjectCitationChipsOptions {
3
+ /**
4
+ * Chips markers whose citations have not arrived yet, as inert placeholders.
5
+ * The citations payload only arrives once the text finishes, so without this
6
+ * a streaming answer shows raw `[^N]` text that snaps into chips at the very
7
+ * end. Placeholders upgrade to real chips when their citation lands; on by
8
+ * the caller only while streaming, so a truly unresolved marker still ends
9
+ * up as literal text.
10
+ */
11
+ pendingMarkers?: boolean;
12
+ }
13
+ /**
14
+ * Replaces resolved `[^N]` markers in `html` with citation chips. A marker
15
+ * with no matching citation is left as literal text, so a hallucinated ref
16
+ * never becomes a dead chip.
17
+ */
18
+ export declare function injectCitationChips(html: string, citations: ChatCitation[], options?: InjectCitationChipsOptions): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Turns HTML character references back into the characters they stand for, so
3
+ * a retrieved passage reading `&#x26;` displays as `&`.
4
+ *
5
+ * Retrieval text arrives with markup-special characters encoded, because the
6
+ * documents behind it were HTML or were extracted through an HTML pipeline.
7
+ * Answer text never shows the problem: it goes through the markdown renderer,
8
+ * which decodes references on its way to HTML. Passages and source titles are
9
+ * rendered as plain text instead, so without this they keep the raw `&#x26;`.
10
+ *
11
+ * SAFETY: the result is text, and must only ever be rendered as text (Vue's
12
+ * `{{ }}`, `textContent`, or an escaped attribute). Decoding is not
13
+ * sanitizing. `&lt;script&gt;` decodes to `<script>`, which is inert in a text
14
+ * node and an injection if handed to `v-html` or `innerHTML`.
15
+ */
16
+ /**
17
+ * Drops HTML tags from retrieved text, keeping only what a reader would see.
18
+ * Extraction pipelines leave passage text with fragments of source markup in
19
+ * it, and a passage is rendered as plain text, where "<p>" would read as
20
+ * literal noise. Strip before decoding entities, so an author's own "&lt;p&gt;"
21
+ * survives as the "<p>" they wrote instead of being taken for a tag.
22
+ */
23
+ export declare function stripHtmlTags(text: string): string;
24
+ /**
25
+ * Decodes character references in `text`. Runs once, not until stable, so
26
+ * text that genuinely reads `&amp;#x26;` decodes to `&#x26;` and stops, the
27
+ * way a browser would. An unrecognized name is left exactly as it was.
28
+ */
29
+ export declare function decodeHtmlEntities(text: string): string;
@@ -0,0 +1,47 @@
1
+ import { type MathSetting } from './math';
2
+ /**
3
+ * Post-processing hook applied to the sanitized DOM before it is serialized.
4
+ * Extension point for product-specific link handling (e.g. rewriting internal
5
+ * links); the default only hardens external links.
6
+ */
7
+ export type SanitizedHtmlPostProcessor = (container: HTMLElement) => void;
8
+ /**
9
+ * Safely render markdown text to HTML
10
+ * Uses marked for parsing and DOMPurify for sanitization
11
+ * Uses setTimeout to yield control and prevent main thread blocking
12
+ *
13
+ * @param text - The markdown text to render
14
+ * @param maxLength - Maximum input length (default: 50000 chars)
15
+ * @returns Promise<Sanitized HTML string>
16
+ */
17
+ export interface RenderMarkdownOptions {
18
+ /** Maximum input length before the text is truncated. */
19
+ maxLength?: number;
20
+ /**
21
+ * Renders `$...$` and `$$...$$` as formulas. Needs the optional `mathlive`
22
+ * peer dependency, loaded on first use. Off by default: `$` is also a
23
+ * currency symbol, so this only belongs on for a corpus that has math.
24
+ */
25
+ showMath?: MathSetting;
26
+ }
27
+ export declare function renderMarkdownSafe(text: string, options?: RenderMarkdownOptions): Promise<string>;
28
+ /**
29
+ * Result of streaming markdown rendering
30
+ */
31
+ export interface StreamingRenderResult {
32
+ html: string;
33
+ isComplete: boolean;
34
+ hasUnsafeBreak: boolean;
35
+ lastSafePoint: number;
36
+ }
37
+ /**
38
+ * streaming markdown renderer
39
+ *
40
+ * @param text - The markdown text to render (potentially incomplete)
41
+ * @param options - Rendering options
42
+ * @returns Promise<StreamingRenderResult> with safe HTML and metadata
43
+ */
44
+ export declare function renderMarkdownStreaming(text: string, options?: {
45
+ forceComplete?: boolean;
46
+ }): Promise<StreamingRenderResult>;
47
+ export default renderMarkdownSafe;
@@ -0,0 +1,36 @@
1
+ import type { RendererExtension, TokenizerExtension } from 'marked';
2
+ export interface MathOptions {
3
+ /**
4
+ * Where MathLive's font files are served from, as a URL path.
5
+ *
6
+ * Most apps should leave this alone and `import 'mathlive/static.css'`
7
+ * instead: that stylesheet declares the font faces itself, with URLs
8
+ * relative to the package, so the bundler emits the font files. This option
9
+ * is for serving them from somewhere else, such as a CDN or an unusual base
10
+ * path, in which case skip the stylesheet import.
11
+ */
12
+ fontsDirectory?: string;
13
+ }
14
+ /** `true` for the defaults, or an object to point at the fonts. */
15
+ export type MathSetting = boolean | MathOptions;
16
+ export declare function mathOptionsOf(setting: MathSetting | undefined): MathOptions | null;
17
+ /**
18
+ * A marked rule that carries a math span through markdown unchanged.
19
+ *
20
+ * Markdown's own escape rule would otherwise get there first and strip the
21
+ * backslash out of any escaped punctuation, so `$\{x\}$` reached MathLive as
22
+ * `${x}$` and `$$\left\{x\right\}$$` as an invalid `\left{`. Only backslashes
23
+ * before punctuation were affected, which is why `\frac` worked and made the
24
+ * problem easy to miss.
25
+ *
26
+ * This claims the span and writes it straight back out, so the text reaching
27
+ * the DOM is exactly what the model wrote. Rendering it is renderMathIn's job.
28
+ */
29
+ export declare const protectMathSource: TokenizerExtension & RendererExtension;
30
+ /**
31
+ * Replaces formulas inside `container` with rendered markup, in place.
32
+ *
33
+ * Text inside `code` and `pre` is left alone: a fenced block showing LaTeX
34
+ * source is showing it deliberately.
35
+ */
36
+ export declare function renderMathIn(container: HTMLElement, options?: MathOptions): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import type { MathSetting } from './math';
2
+ /**
3
+ * Manages streaming markdown rendering for a single message. Collects text as
4
+ * it arrives and re-renders it on a throttle, so the markdown parse runs at
5
+ * most a few dozen times a second no matter how fast the text comes in.
6
+ * Framework-free; the consumer wires `onUpdate` into its own reactive state.
7
+ */
8
+ export default class StreamingMessage {
9
+ private onUpdate;
10
+ private throttleMs;
11
+ /** Renders formulas on every pass, so they typeset as the answer arrives. */
12
+ private showMath;
13
+ private buffer;
14
+ private renderTimer;
15
+ private destroyed;
16
+ private stopped;
17
+ constructor(onUpdate: (html: string, isComplete: boolean) => void, throttleMs?: number, // up to 40 renders a second
18
+ /** Renders formulas on every pass, so they typeset as the answer arrives. */
19
+ showMath?: MathSetting);
20
+ addChunk(chunk: string): void;
21
+ /** Renders everything received so far and marks the message finished. */
22
+ complete(): Promise<void>;
23
+ private render;
24
+ /** The raw markdown received so far. */
25
+ getCurrentContent(): string;
26
+ /**
27
+ * Stops further renders. Whatever is already on screen stays there, so a
28
+ * partial answer is never blanked; the caller owns the final render.
29
+ */
30
+ stop(): void;
31
+ destroy(): void;
32
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Shared SSE (Server-Sent Events) streaming service for Base Camp chat.
3
+ *
4
+ * Centralises the fetch -> ReadableStream -> SSE line-parsing pipeline. Consumers
5
+ * provide a small `eventHandler` that maps Base Camp's domain-specific events
6
+ * (`message_id`, `assistant_message_id`, `token_usage`, `used_sources`) to their own
7
+ * callbacks. Common events (`response_delta`, `thinking_*`, `text_complete`, `error`)
8
+ * are handled by the built-in defaults.
9
+ *
10
+ * Base Camp streams SSE over the POST response body, so `EventSource` (GET-only)
11
+ * cannot be used; the stream is parsed manually here.
12
+ */
13
+ export interface StreamingCallbacks {
14
+ /** Receives each text chunk of the streamed response. */
15
+ onChunk: (chunk: string) => void;
16
+ /** Called when the stream ends normally (`[DONE]` or reader exhausted). */
17
+ onComplete: () => void;
18
+ /** Called on any unrecoverable error (network, parse, HTTP status). */
19
+ onError: (error: Error) => void;
20
+ onMetadata?: (metadata: {
21
+ messageId?: string;
22
+ sources?: unknown[];
23
+ hasSourceContext?: boolean;
24
+ }) => void;
25
+ onThinkingStart?: () => void;
26
+ onThinkingChunk?: (chunk: string) => void;
27
+ onThinkingEnd?: () => void;
28
+ onTextComplete?: () => void;
29
+ onTokenUsage?: (percentOfCumulativeLimit: number) => void;
30
+ /** Called with the message quota parsed from X-RateLimit-* response headers. */
31
+ onRateLimit?: (info: RateLimitInfo) => void;
32
+ }
33
+ /** Message quota reported by Base Camp's X-RateLimit-* headers. */
34
+ export interface RateLimitInfo {
35
+ limit: number;
36
+ remaining: number;
37
+ /** Epoch seconds of the next quota reset. */
38
+ resetSeconds: number;
39
+ }
40
+ /** Thrown when Base Camp responds 429: the caller's message quota is spent. */
41
+ export declare class RateLimitError extends Error {
42
+ limit: number;
43
+ remaining: number;
44
+ /** ISO timestamp of the next quota reset. */
45
+ resetAt: string;
46
+ constructor(body: {
47
+ error?: string;
48
+ limit?: number;
49
+ remaining?: number;
50
+ resetAt?: string;
51
+ });
52
+ }
53
+ /**
54
+ * Return `true` when the handler consumed the event so the built-in default
55
+ * handler is skipped. Return `false`/`undefined` to let the default run.
56
+ */
57
+ export type SSEEventHandler = (eventType: string, data: unknown, callbacks: StreamingCallbacks) => boolean | void;
58
+ export interface StreamSSEOptions {
59
+ /** AbortSignal to cancel the request (e.g. a stop button). */
60
+ signal?: AbortSignal;
61
+ /**
62
+ * Extra request headers merged over the base streaming headers. The app
63
+ * boundary supplies deployment identity / tenant headers here; the kit
64
+ * itself stays config-free.
65
+ */
66
+ headers?: Record<string, string>;
67
+ }
68
+ /**
69
+ * Opens an SSE connection via `fetch`, reads the stream, and dispatches events.
70
+ * Cookies are sent (`credentials: 'include'`) so Base Camp can resolve the session user.
71
+ *
72
+ * @param url Full URL of the Base Camp streaming endpoint.
73
+ * @param body JSON-serialisable request body.
74
+ * @param callbacks Lifecycle callbacks the consumer cares about.
75
+ * @param eventHandler Optional Base Camp event router; return `true` to skip defaults.
76
+ * @param options Optional settings (AbortSignal, extra headers, etc.).
77
+ */
78
+ /**
79
+ * Reads an already-made streaming response. Use this when something else,
80
+ * like the Base Camp client, performed the request and handed back the raw
81
+ * Response. Reports quota headers, then feeds every event to the callbacks.
82
+ */
83
+ export declare function consumeSSEResponse(response: Response, callbacks: StreamingCallbacks, eventHandler?: SSEEventHandler): Promise<void>;
84
+ export declare function streamSSE(url: string, body: unknown, callbacks: StreamingCallbacks, eventHandler?: SSEEventHandler, options?: StreamSSEOptions): Promise<void>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Paces streamed text so it reads as steady typing.
3
+ *
4
+ * Base Camp's stream often arrives in bursts: nothing for a while, then a
5
+ * large block at once. Writing a burst straight to the screen makes the
6
+ * answer lurch. This sits between the network and the message, queueing
7
+ * whatever arrives and letting it out a few characters per frame.
8
+ *
9
+ * The pace is constant: the same characters per frame from the first word to
10
+ * the last, so the reveal never visibly accelerates. Hidden tabs skip the
11
+ * animation entirely, because browsers suspend frame callbacks there and the
12
+ * backlog would replay on return.
13
+ */
14
+ export interface TypewriterOptions {
15
+ /**
16
+ * Pace multiplier. 1 is the kit's default feel, 2 types twice as fast, 0.5
17
+ * at half. It scales the whole behavior, trickle and burst-drain alike, so
18
+ * the reveal keeps its character at any setting. A value that makes no pace
19
+ * (zero, negative, not a number) falls back to 1.
20
+ */
21
+ speed?: number;
22
+ }
23
+ /** `false` shows text the instant it arrives; `true` or options turn pacing on. */
24
+ export type TypewriterSetting = boolean | TypewriterOptions;
25
+ export default class Typewriter {
26
+ private emit;
27
+ private queue;
28
+ /** Characters revealed per frame, fixed for the life of the reveal. */
29
+ private readonly charsPerFrame;
30
+ /**
31
+ * Budget left over from earlier frames. Fractional speeds need it: at 0.5
32
+ * the budget is half a character per frame, which becomes one character
33
+ * every other frame instead of none at all.
34
+ */
35
+ private carry;
36
+ private readonly enabled;
37
+ private frameHandle;
38
+ private waiters;
39
+ private destroyed;
40
+ private readonly onVisibilityChange;
41
+ constructor(emit: (text: string) => void, setting?: TypewriterSetting);
42
+ /** Queues text for the reveal. Emits right away when pacing is off or the tab is hidden. */
43
+ push(text: string): void;
44
+ /** Emits everything still queued, immediately and synchronously. */
45
+ flush(): void;
46
+ /** Resolves once the queue has drained at its own pace. */
47
+ finish(): Promise<void>;
48
+ destroy(): void;
49
+ private schedule;
50
+ /** Emits one frame's worth of characters. */
51
+ private step;
52
+ private cancelFrame;
53
+ private resolveWaiters;
54
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A stand-in for Base Camp, for tests and for developing the interface before
3
+ * the real backend is reachable. It answers like the real thing does: a
4
+ * streaming response delivered a few words at a time, with the same event
5
+ * names on the wire, so the whole reading path gets exercised.
6
+ */
7
+ import type { ChatTransport } from '../composables/useChatEngine';
8
+ import type { AgentDetails, ChatCitation, CreateThreadBody, CreateThreadResponse, FeedbackResponse, MessageFeedback, MessageListResponse, QuotaResponse, SendMessageBody, StopMessageResponse, Thread, TokenUsage, UsedSource } from '../types';
9
+ export interface MockReply {
10
+ text: string;
11
+ sources?: UsedSource[];
12
+ /** Emitted as the citations event, the way inline-citation agents answer. */
13
+ citations?: ChatCitation[];
14
+ /** Streamed as thinking events ahead of the answer, like a reasoning agent. */
15
+ thinking?: string;
16
+ /** Emitted as the token_usage event after the answer completes. */
17
+ tokenUsage?: TokenUsage;
18
+ }
19
+ export interface MockTransportOptions {
20
+ /** Decides what the assistant says back. Defaults to a canned answer. */
21
+ replyTo?: (userText: string) => MockReply;
22
+ /** Pause between streamed pieces, in milliseconds. */
23
+ chunkDelayMs?: number;
24
+ /** Pause before the first piece, imitating retrieval time. */
25
+ thinkingDelayMs?: number;
26
+ /**
27
+ * A message allowance to report, counting down as messages are sent.
28
+ * Left out, the mock answers 'unlimited', the way an uncapped tenant does.
29
+ */
30
+ messageLimit?: number;
31
+ }
32
+ export declare class MockTransport implements ChatTransport {
33
+ private options;
34
+ private threadCount;
35
+ private messageCount;
36
+ private history;
37
+ /** Fixed for the life of the mock, so a test can assert on it. */
38
+ private readonly quotaResetAt;
39
+ constructor(options?: MockTransportOptions);
40
+ getAgent(agentId: string): Promise<AgentDetails>;
41
+ createThread(_agentId: string, _body?: CreateThreadBody): Promise<CreateThreadResponse>;
42
+ /** Newest first and capped at `limit`, the way the server pages. */
43
+ getThreads(agentId: string, limit: number): Promise<Thread[]>;
44
+ getQuota(_agentId: string): Promise<QuotaResponse>;
45
+ getMessages(_agentId: string, threadId: string): Promise<MessageListResponse>;
46
+ sendMessage(_agentId: string, _threadId: string, body: SendMessageBody, options?: {
47
+ signal?: AbortSignal;
48
+ }): Promise<Response>;
49
+ stopMessage(): Promise<StopMessageResponse>;
50
+ submitFeedback(_agentId: string, _threadId: string, _messageId: string, feedback: MessageFeedback): Promise<FeedbackResponse>;
51
+ clearFeedback(): Promise<FeedbackResponse>;
52
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Fills in the two browser APIs jsdom leaves out that kit components use.
3
+ *
4
+ * Both are universally available in real browsers, so the components are right
5
+ * to call them unguarded; the gap is jsdom's. Stubbing here keeps the
6
+ * workaround out of the components.
7
+ */
8
+ export {};
package/dist/theme.css ADDED
@@ -0,0 +1,122 @@
1
+ /*
2
+ * The kit's theming contract: every custom property its components read, with
3
+ * a neutral default so the chat is legible before an app themes anything.
4
+ *
5
+ * Import it once, then override whatever you want from your own stylesheet:
6
+ *
7
+ * import '@twentyfourg/chat-kit/theme.css';
8
+ *
9
+ * These defaults are written to lose. They sit in a cascade layer, which puts
10
+ * them behind any unlayered rule, and they are matched through :where(), which
11
+ * zeroes their specificity. An ordinary `:root { --accent: ... }` in the app
12
+ * beats them wherever the import sits.
13
+ *
14
+ * An app that keeps its own tokens in a layer is the exception, since layer
15
+ * order is settled before specificity. Such an app should import this file
16
+ * first, or pin the order with `@layer chat-kit.theme, app;`.
17
+ *
18
+ * Many values below point at another token rather than a literal, so most
19
+ * rebrands only need the handful marked SEED. Setting --accent, --accent-fg,
20
+ * --text-primary, --surface-card and --font-body gets you most of the way.
21
+ *
22
+ * A test (src/__tests__/theme-tokens.spec.ts) fails if a component reads a
23
+ * property that is missing here, or if a property here is read by nothing, so
24
+ * this file cannot drift out of step with the components.
25
+ */
26
+
27
+ @layer chat-kit.theme {
28
+ :where(:root) {
29
+ /* --- Type ------------------------------------------------------------ */
30
+
31
+ /* SEED. The other three families follow it unless set. */
32
+ --font-body:
33
+ ui-sans-serif, system-ui, -apple-system, "Segoe UI", roboto, helvetica, arial, sans-serif;
34
+ --font-display: var(--font-body);
35
+ --font-label: var(--font-body);
36
+ --font-ui: var(--font-body);
37
+ --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", menlo, consolas, monospace;
38
+
39
+ --t-xs: 12px;
40
+ --t-sm: 14px;
41
+ --t-base: 16px;
42
+ --t-2xl: 24px;
43
+
44
+ --lh-body: 1.6;
45
+ /* Fixed rather than a ratio: the composer measures its own line height. */
46
+ --lh-sm: 20px;
47
+ --tr-tight: -0.02em;
48
+
49
+ --fw-regular: 400;
50
+ --fw-bold: 700;
51
+
52
+ /* --- Spacing and shape ---------------------------------------------- */
53
+
54
+ --spacing-1: 4px;
55
+ --spacing-2: 8px;
56
+ --spacing-3: 12px;
57
+ --spacing-4: 16px;
58
+ --spacing-5: 20px;
59
+ --spacing-6: 24px;
60
+
61
+ --r-sm: 4px;
62
+ --r-md: 5px;
63
+ --r-lg: 8px;
64
+ --r-pill: 100px;
65
+
66
+ /* Cards inside the chat: the citation popover and the hover labels. */
67
+ --card-radius: 10px;
68
+ --card-shadow: 0 1px 3px rgb(0 0 0 / 8%);
69
+ --card-stroke: #d7dae0;
70
+ /* Dims the page behind the page-image dialog in list mode. */
71
+ --overlay-scrim: rgb(20 22 26 / 50%);
72
+
73
+ /* --- Surfaces ------------------------------------------------------- */
74
+
75
+ /* SEED. The chat's own background; also the popover and label chips. */
76
+ --surface-card: #ffffff;
77
+ /* Code blocks, quotes, and table headers inside an answer. */
78
+ --surface-raised: #f4f5f7;
79
+ --surface-hover: #e9ebef;
80
+
81
+ /* --- Text ----------------------------------------------------------- */
82
+
83
+ /* SEED. */
84
+ --text-primary: #14161a;
85
+ --text-secondary: #5c626d;
86
+ /* Small labels above the sources and the feedback row. */
87
+ --text-helper: rgb(20 22 26 / 55%);
88
+
89
+ /* --- Accent --------------------------------------------------------- */
90
+
91
+ /* SEED. The assistant avatar, links, citation chips, and the send button.
92
+ Replace this pair first; a lot of the chat follows it. */
93
+ --accent: #2d6cdf;
94
+ --accent-fg: #ffffff;
95
+
96
+ /* --- The chat's own parts ------------------------------------------- */
97
+
98
+ --user-bubble-bg: #eef1f6;
99
+ --user-bubble-fg: var(--text-primary);
100
+
101
+ --composer-bg: #f4f5f7;
102
+ --composer-fg: var(--text-primary);
103
+ --composer-placeholder: rgb(20 22 26 / 60%);
104
+
105
+ /* Split from --accent so a chip can be toned down without moving the
106
+ accent itself. */
107
+ --citation-bg: var(--accent);
108
+
109
+ --icon-button-bg: #e9ebef;
110
+ --icon-button-bg-hover: #dcdfe5;
111
+ /* Send once the draft has text, and the mic while it is listening. */
112
+ --icon-button-bg-active: var(--accent);
113
+ --icon-button-bg-active-hover: color-mix(in srgb, var(--accent) 70%, transparent);
114
+ --icon-button-fg-active: var(--accent-fg);
115
+
116
+ --scrollbar-thumb: rgb(20 22 26 / 20%);
117
+ --scrollbar-thumb-hover: rgb(20 22 26 / 35%);
118
+
119
+ /* Failed answers and the unsupported-mic marker. */
120
+ --warning: #d3383d;
121
+ }
122
+ }