quassel 0.1.0 → 0.2.0

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 (59) hide show
  1. package/ChatInputToolbar.d.ts +35 -6
  2. package/ChatMessages.d.ts +19 -15
  3. package/ChatPanel.d.ts +4 -2
  4. package/ChatSendContext.d.ts +7 -12
  5. package/DetailModeSwitch.d.ts +1 -1
  6. package/Markdown.d.ts +4 -11
  7. package/MessageActions.d.ts +1 -10
  8. package/PendingActionCard.d.ts +13 -0
  9. package/QuasselProvider.d.ts +76 -0
  10. package/README.md +39 -38
  11. package/StepPopover.d.ts +1 -1
  12. package/TimestampSwitch.d.ts +2 -2
  13. package/WorkingScenes.d.ts +3 -1
  14. package/attachments.d.ts +24 -0
  15. package/chat-attachments.d.ts +6 -0
  16. package/chat-scroll.d.ts +12 -0
  17. package/chat.css +2051 -0
  18. package/chunk-IGCFQJZB.js +183 -0
  19. package/chunk-IGCFQJZB.js.map +7 -0
  20. package/events.d.ts +74 -17
  21. package/events.js +11 -1
  22. package/index.d.ts +17 -10
  23. package/index.js +1615 -980
  24. package/index.js.map +4 -4
  25. package/options.d.ts +1 -1
  26. package/package.json +12 -10
  27. package/texts.d.ts +38 -11
  28. package/timestamps.d.ts +6 -4
  29. package/types.d.ts +6 -6
  30. package/ui/button.d.ts +8 -0
  31. package/ui/card.d.ts +5 -0
  32. package/ui/cn.d.ts +2 -0
  33. package/ui/popover.d.ts +4 -0
  34. package/ui/spinner.d.ts +2 -0
  35. package/ui/stop-button.d.ts +10 -0
  36. package/ui/toggle.d.ts +8 -0
  37. package/ui/useFileInput.d.ts +20 -0
  38. package/useChatAction.d.ts +6 -0
  39. package/QuestionCard.d.ts +0 -12
  40. package/chunk-DOLTANTM.js +0 -84
  41. package/chunk-DOLTANTM.js.map +0 -7
  42. package/css/chat.css +0 -927
  43. package/css/foundation/base.css +0 -24
  44. package/css/foundation/index.css +0 -3
  45. package/css/foundation/panel.css +0 -111
  46. package/css/foundation/tokens.css +0 -68
  47. package/css/foundation/wash.css +0 -26
  48. package/css/question.css +0 -149
  49. package/icons.d.ts +0 -15
  50. package/server/http.d.ts +0 -17
  51. package/server/index.d.ts +0 -6
  52. package/server/index.js +0 -478
  53. package/server/index.js.map +0 -7
  54. package/server/manager.d.ts +0 -12
  55. package/server/openai.d.ts +0 -16
  56. package/server/session.d.ts +0 -27
  57. package/server/store.d.ts +0 -18
  58. package/server/types.d.ts +0 -62
  59. package/useChat.d.ts +0 -14
@@ -1,5 +1,6 @@
1
- import { ReactNode } from "react";
1
+ import { ReactNode, Ref } from "react";
2
2
  import { ChatTexts } from "./texts";
3
+ import type { ChatAttachmentInput } from "./events";
3
4
  import type { SendShortcut } from "./options";
4
5
  /** Ein Toolbar-Knopf, deklarativ: Symbol, Text oder beides - plus Klick-Handler. */
5
6
  export interface ToolbarAction {
@@ -10,23 +11,51 @@ export interface ToolbarAction {
10
11
  active?: boolean;
11
12
  onClick: () => void;
12
13
  }
14
+ /** Hosts steuern den Entwurf von aussen, z.B. um eine Vorlage zum Bearbeiten einzusetzen. */
15
+ export interface ChatInputHandle {
16
+ /** Ersetzt den Entwurf, fokussiert die Eingabe und stellt die Schreibmarke ans Ende. */
17
+ insert: (text: string) => void;
18
+ focus: () => void;
19
+ reset: () => void;
20
+ submit: (onSubmit?: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>, options?: {
21
+ allowEmpty?: boolean;
22
+ }) => Promise<void>;
23
+ }
13
24
  /**
14
25
  * Die Eingabe-Karte: rahmenlose Textarea oben, Toolbar unter einer Haarlinie. Eigene
15
- * Knöpfe kommen deklarativ über `actions` oder frei über die Slots links/rechts; im Lauf
16
- * bleibt der Stop-Knopf sichtbar, und Tippen zeigt daneben den Senden-Knopf, der zum
17
- * Dazwischenfunken wird. `rows` bestimmt die Starthöhe; mit `maxRows` wächst die Textarea
26
+ * Knöpfe kommen deklarativ über `actions` oder frei über die Slots links/rechts; bei
27
+ * laufender Arbeit bleibt der Stop-Knopf sichtbar, und Tippen zeigt daneben den Senden-Knopf,
28
+ * der zum Dazwischenfunken wird. `rows` bestimmt die Starthöhe; mit `maxRows` wächst die Textarea
18
29
  * mit dem Inhalt bis zu dieser Zeilenzahl und scrollt danach.
19
30
  */
20
- export declare function ChatInputToolbar({ onSend, onStop, running, disabled, rows, maxRows, actions, toolbarLeft, toolbarRight, texts, sendShortcut, }: {
21
- onSend: (text: string) => void | Promise<void>;
31
+ export declare function ChatInputToolbar({ onSend, onStop, running, disabled, sendDisabled, layout, detailsContainer, onErrorChange, inputAriaControls, rows, maxRows, actions, toolbarLeft, toolbarRight, texts, handleRef, attachments: attachmentsEnabled, attachmentCapabilities, attachmentCapabilitiesError, onAttachmentsChange, initialValue, onDraftChange, sendShortcut, autoFocus, onAutoFocusSettled, }: {
32
+ onSend: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>;
22
33
  onStop?: () => void;
23
34
  running?: boolean;
24
35
  disabled?: boolean;
36
+ sendDisabled?: boolean;
37
+ layout?: "card" | "toolbar" | "inline";
38
+ detailsContainer?: HTMLElement | null;
39
+ onErrorChange?: (error: string | undefined) => void;
40
+ inputAriaControls?: string;
25
41
  rows?: number;
26
42
  maxRows?: number;
27
43
  actions?: ToolbarAction[];
28
44
  toolbarLeft?: ReactNode;
29
45
  toolbarRight?: ReactNode;
30
46
  texts?: Partial<ChatTexts>;
47
+ handleRef?: Ref<ChatInputHandle>;
48
+ /** false = kein Anhängen: weder Knopf noch Ablegen oder Einfügen von Dateien. */
49
+ attachments?: boolean;
50
+ attachmentCapabilities?: {
51
+ input: readonly string[];
52
+ model: string;
53
+ };
54
+ attachmentCapabilitiesError?: string;
55
+ onAttachmentsChange?: (hasAttachments: boolean) => void;
56
+ initialValue?: string;
57
+ onDraftChange?: (text: string) => void;
31
58
  sendShortcut?: SendShortcut;
59
+ autoFocus?: boolean;
60
+ onAutoFocusSettled?: () => void;
32
61
  }): import("react").JSX.Element;
package/ChatMessages.d.ts CHANGED
@@ -1,40 +1,44 @@
1
- import { ReactNode } from "react";
2
- import { type ChatAppearance, type TimestampOptions, type CodeBlockOptions, type BubbleOptions, type MessageActionsOptions } from "./options";
3
- import { ChatAnnouncement, DetailMode, Message, ToolInfo } from "./types";
1
+ import { type CSSProperties, ReactNode } from "react";
2
+ import { ChatAnnouncement, DetailMode, Message, PendingAction, ToolInfo } from "./types";
4
3
  import { ChatTexts } from "./texts";
5
4
  import { type LinkClickHandler } from "./Markdown";
5
+ import { type ChatAppearance, type TimestampOptions, type CodeBlockOptions, type BubbleOptions, type MessageActionsOptions } from "./options";
6
6
  /**
7
7
  * Der Verlauf: eigener Scroll-Container mit Autoscroll, solange man unten ist,
8
8
  * und einem Zum-Ende-Knopf, sobald man liest. Ohne Eingabe read-only nutzbar.
9
9
  */
10
- export declare function ChatMessages({ messages, detailMode, running, working, workingTimeoutMs, stepsExpandable, texts, toolArgumentsText, renderTool, onAnswerQuestion, onLinkClick, announce, emptyState, className, showTimestamps, bottomThreshold, scrollerRef, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, }: {
10
+ export declare function ChatMessages({ messages, owner, detailMode, running, working, stepsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, }: {
11
11
  messages: Message[];
12
- appearance?: ChatAppearance;
13
- timestampOptions?: TimestampOptions;
14
- codeBlockOptions?: CodeBlockOptions;
15
- bubbleOptions?: BubbleOptions;
16
- messageActions?: MessageActionsOptions;
12
+ /** Beiträge mit diesem Message.sender erscheinen ohne Sprechblase; null behält die Nachrichtenvorgaben. */
13
+ owner?: string | null;
17
14
  detailMode?: DetailMode;
18
15
  running?: boolean;
19
16
  working?: ReactNode;
20
- /** Ohne neue Events verschwindet der Working-Indikator nach dieser Zeit, Default 20s. */
21
- workingTimeoutMs?: number;
22
17
  /** false = Denk- und Werkzeug-Schritte lassen sich nicht aufklappen (kein Popover). */
23
18
  stepsExpandable?: boolean;
24
19
  texts?: Partial<ChatTexts>;
25
20
  toolArgumentsText?: (tool: ToolInfo) => string;
26
21
  /** Eigene Darstellung fuer einzelne Werkzeuge; undefined = Standarddarstellung. */
27
22
  renderTool?: (tool: ToolInfo) => ReactNode | undefined;
28
- onAnswerQuestion?: (callId: string, text: string) => void;
23
+ /** Darstellung einer wartenden Aktion durch ihren Eigentümer; undefined = generische Karte. */
24
+ renderAction?: (action: PendingAction, text: string) => ReactNode | undefined;
25
+ onDismissAction?: (actionId: string) => void;
29
26
  /** Faengt Klicks auf Markdown-Links ab; true = behandelt, der Browser folgt nicht. */
30
27
  onLinkClick?: LinkClickHandler;
31
- /** Screenreader-Ansage je fertiger Antwort und neuer Rückfrage: Default ist ihr Text, eine Funktion liefert eigenen Text (undefined = still), false schaltet ab. */
32
- announce?: false | ((announcement: ChatAnnouncement) => string | undefined);
33
28
  emptyState?: ReactNode;
34
29
  className?: string;
35
30
  /** true = dezente HH:MM-Spalte links an jedem Block (Message.at). */
36
31
  showTimestamps?: boolean;
37
- bottomThreshold?: number;
32
+ /** Screenreader-Ansage fertiger Antworten und neuer Aktionen; false = der Host sagt selbst an. */
33
+ announce?: false | ((announcement: ChatAnnouncement) => string | undefined);
38
34
  /** Exposes the scroll container so hosts can scroll without querying the DOM. */
39
35
  scrollerRef?: (element: HTMLDivElement | null) => void;
36
+ maxWidth?: CSSProperties["maxWidth"];
37
+ horizontalPadding?: CSSProperties["paddingInline"];
38
+ jumpToEndThreshold?: number;
39
+ appearance?: ChatAppearance;
40
+ timestampOptions?: TimestampOptions;
41
+ codeBlockOptions?: CodeBlockOptions;
42
+ bubbleOptions?: BubbleOptions;
43
+ messageActions?: MessageActionsOptions;
40
44
  }): import("react").JSX.Element;
package/ChatPanel.d.ts CHANGED
@@ -1,15 +1,17 @@
1
- import { type ReactNode, type Ref } from "react";
1
+ import { type CSSProperties, type ReactNode, type Ref } from "react";
2
2
  import { type ChatAppearance } from "./options";
3
3
  /**
4
4
  * Verlauf mit ueberlagerter Eingabe. Der Rahmen misst die Eingabe selbst und
5
5
  * gibt dem Verlauf den noetigen Fussraum, damit kein Host Innenmasse setzen muss.
6
6
  */
7
- export declare function ChatPanel({ children, composer, className, nodeRef, appearance, scrollOnSend, }: {
7
+ export declare function ChatPanel({ children, composer, className, nodeRef, maxWidth, horizontalPadding, appearance, scrollOnSend, }: {
8
8
  children: ReactNode;
9
9
  /** Die Eingabe; sie liegt ueber dem Verlauf, nicht darunter. */
10
10
  composer?: ReactNode;
11
11
  className?: string;
12
12
  nodeRef?: Ref<HTMLDivElement>;
13
+ maxWidth?: CSSProperties["maxWidth"];
14
+ horizontalPadding?: CSSProperties["paddingInline"];
13
15
  appearance?: ChatAppearance;
14
16
  scrollOnSend?: boolean;
15
17
  }): import("react").JSX.Element;
@@ -1,12 +1,7 @@
1
- import type { KeyboardEvent } from "react";
2
- import type { SendShortcut } from "./options";
3
- export declare const ChatSendContext: import("react").Context<{
4
- onSent: () => void;
5
- registerJump: (jump: () => void) => () => void;
6
- } | undefined>;
7
- export declare function isSendKey(event: KeyboardEvent<HTMLTextAreaElement>, shortcut: SendShortcut): boolean;
8
- export declare function useChatSubmit(onSend: (text: string) => void | Promise<void>): {
9
- submit: (text: string) => Promise<boolean>;
10
- sending: boolean;
11
- error: string | undefined;
12
- };
1
+ import { type RefObject } from "react";
2
+ export interface ChatSendScope {
3
+ notify: () => void;
4
+ subscribe: (listener: () => void) => () => void;
5
+ }
6
+ export declare const ChatSendContext: import("react").Context<ChatSendScope | undefined>;
7
+ export declare function createChatSendScope(enabled: RefObject<boolean>): ChatSendScope;
@@ -9,7 +9,7 @@ export declare const detailModeLabel: (mode: DetailMode, texts?: Partial<ChatTex
9
9
  export declare function DetailModeSwitch({ mode, onChange, modes, collapsible, texts, className, }: {
10
10
  mode: DetailMode;
11
11
  onChange: (mode: DetailMode) => void;
12
- /** Auswahl und Reihenfolge des Weiterschaltens; Default sind alle sechs Modi. */
12
+ /** Auswahl und Reihenfolge des Weiterschaltens; Default sind alle Modi. */
13
13
  modes?: readonly DetailMode[];
14
14
  /** false = die Beschriftung bleibt auch in einer schmalen Eingabe-Karte stehen. */
15
15
  collapsible?: boolean;
package/Markdown.d.ts CHANGED
@@ -1,26 +1,19 @@
1
1
  import { ReactNode } from "react";
2
2
  import type { CodeBlockOptions } from "./options";
3
3
  import { type ChatTexts } from "./texts";
4
+ export type LinkClickHandler = (href: string, label: string) => boolean;
4
5
  export declare function MarkdownCodeBlocks({ children, options, texts }: {
5
6
  children: ReactNode;
6
7
  options?: CodeBlockOptions;
7
- texts?: Partial<ChatTexts>;
8
+ texts: ChatTexts;
8
9
  }): import("react").JSX.Element;
9
- /**
10
- * Schlanker Markdown-Renderer ohne Abhängigkeit: Überschriften, **fett**, *kursiv*,
11
- * `code`, Links, Listen, Blockquotes, Code-Blöcke und Tabellen. Bewusst sicher (reines
12
- * React, kein dangerouslySetInnerHTML) und streaming-robust: zeilenbasiert, unvollständige
13
- * Blöcke werden so weit gerendert wie vorhanden.
14
- */
15
- /** true = der Host hat den Link behandelt, der Browser folgt ihm nicht. */
16
- export type LinkClickHandler = (href: string, label: string) => boolean;
17
- /** Faengt Klicks auf Markdown-Links im Teilbaum ab, z.B. fuer eigene Ziele der Gastseite. */
18
10
  export declare function MarkdownLinks({ children, onLinkClick }: {
19
11
  children: ReactNode;
20
12
  onLinkClick?: LinkClickHandler;
21
13
  }): import("react").JSX.Element;
22
- export declare function Markdown({ text }: {
14
+ export declare function Markdown({ text, streaming }: {
23
15
  text: string;
16
+ streaming?: boolean;
24
17
  }): import("react").JSX.Element;
25
18
  /** Dieselbe Syntax wie der Renderer, als Klartext ohne Markup, etwa für Screenreader-Ansagen. */
26
19
  export declare function markdownPlainText(text: string): string;
@@ -1,17 +1,8 @@
1
- import { type ReactNode } from "react";
2
1
  import type { MessageActionsOptions } from "./options";
3
2
  import type { ChatTexts } from "./texts";
4
3
  import type { Message } from "./types";
5
- export declare function ActionButton({ label, icon, disabled, onClick, successLabel }: {
6
- label: string;
7
- icon?: ReactNode;
8
- disabled?: boolean;
9
- onClick: () => void | Promise<void>;
10
- successLabel?: string;
11
- }): import("react").JSX.Element;
12
- export declare function copyText(text: string, error: string): Promise<void>;
13
4
  export declare function MessageActions({ message, options, texts }: {
14
5
  message: Message;
15
6
  options?: MessageActionsOptions;
16
7
  texts: ChatTexts;
17
- }): import("react").JSX.Element | null;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import type { PendingAction } from "./types";
2
+ /**
3
+ * Die Darstellung einer wartenden Aktion ohne Beitrag ihres Eigentümers: Titel, der Hinweis,
4
+ * dass eine Eingabe erwartet wird, und das Verwerfen. Die Form kennt nur das Plugin.
5
+ */
6
+ export declare function PendingActionCard({ text, action, waitingLabel, dismissLabel, dismissedLabel, onDismiss, }: {
7
+ text: string;
8
+ action: PendingAction;
9
+ waitingLabel: string;
10
+ dismissLabel: string;
11
+ dismissedLabel: string;
12
+ onDismiss?: () => void;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,76 @@
1
+ import { type ComponentType, type ReactNode } from "react";
2
+ export type QuasselButtonVariant = "default" | "outline" | "secondary" | "ghost" | "destructive" | "link";
3
+ export type QuasselButtonSize = "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg";
4
+ export interface QuasselButtonProps {
5
+ variant?: QuasselButtonVariant;
6
+ size?: QuasselButtonSize;
7
+ className?: string;
8
+ disabled?: boolean;
9
+ title?: string;
10
+ "aria-label"?: string;
11
+ "aria-pressed"?: boolean;
12
+ onClick?: () => void;
13
+ children?: ReactNode;
14
+ }
15
+ export interface QuasselToggleProps {
16
+ variant?: "default" | "outline";
17
+ size?: "default" | "sm" | "lg";
18
+ className?: string;
19
+ disabled?: boolean;
20
+ title?: string;
21
+ pressed?: boolean;
22
+ onPressedChange?: (pressed: boolean) => void;
23
+ children?: ReactNode;
24
+ }
25
+ export interface QuasselCardProps {
26
+ size?: "default" | "sm";
27
+ className?: string;
28
+ children?: ReactNode;
29
+ }
30
+ export interface QuasselStopButtonProps {
31
+ label: string;
32
+ busy?: boolean;
33
+ size?: QuasselButtonSize;
34
+ className?: string;
35
+ disabled?: boolean;
36
+ title?: string;
37
+ onClick?: () => void;
38
+ }
39
+ export interface QuasselPopoverProps {
40
+ open?: boolean;
41
+ onOpenChange?: (open: boolean) => void;
42
+ children?: ReactNode;
43
+ }
44
+ export interface QuasselPopoverAnchor {
45
+ getBoundingClientRect: () => DOMRect;
46
+ contextElement?: Element;
47
+ }
48
+ export interface QuasselPopoverContentProps {
49
+ anchor?: QuasselPopoverAnchor;
50
+ align?: "start" | "center" | "end";
51
+ side?: "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end";
52
+ sideOffset?: number;
53
+ collisionPadding?: number;
54
+ className?: string;
55
+ "aria-label"?: string;
56
+ children?: ReactNode;
57
+ }
58
+ /** The primitives quassel renders; a host replaces any of them through QuasselProvider. */
59
+ export interface QuasselComponents {
60
+ Button: ComponentType<QuasselButtonProps>;
61
+ Toggle: ComponentType<QuasselToggleProps>;
62
+ Card: ComponentType<QuasselCardProps>;
63
+ StopButton: ComponentType<QuasselStopButtonProps>;
64
+ Popover: ComponentType<QuasselPopoverProps>;
65
+ PopoverContent: ComponentType<QuasselPopoverContentProps>;
66
+ }
67
+ export declare const defaultComponents: QuasselComponents;
68
+ export declare function QuasselProvider({ components, allowUrl, children }: {
69
+ /** Replaces single primitives; the rest stay the built-in ones or those of an outer provider. */
70
+ components?: Partial<QuasselComponents>;
71
+ /** Keeps Markdown link and image URLs that the default policy (http, https, mailto, tel, ftp, irc, xmpp, relative) would drop. */
72
+ allowUrl?: (url: string) => boolean;
73
+ children: ReactNode;
74
+ }): import("react").JSX.Element;
75
+ export declare const useQuasselComponents: () => QuasselComponents;
76
+ export declare const useAllowUrl: () => ((url: string) => boolean) | undefined;
package/README.md CHANGED
@@ -1,30 +1,33 @@
1
1
  # quassel
2
2
 
3
- Composable chat building blocks for React: a streaming transcript with thinking and tool steps,
4
- clarifying questions, an input card, a small CSS foundation, and an event contract with a
5
- ready-made backend for OpenAI-compatible endpoints. One package, no runtime dependencies.
3
+ Chat building blocks for React: a streaming transcript with thinking and tool steps, pending
4
+ actions, an input card with attachments and steering, and a small event contract that works
5
+ on the server as well. quassel is the chat of [RAgents](https://github.com/SchlenkR/RAgents),
6
+ published as a library. It ships a compiled stylesheet, so hosts need no Tailwind, and every
7
+ color, radius and font is a `--qsl-*` variable.
6
8
 
7
9
  ![An agent chat with grouped steps, timestamps, Markdown and the input card](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/agent.png)
8
10
 
9
11
  ## What you can build
10
12
 
11
13
  **Agent chats that show their work.** Thinking and tool calls appear as steps. Pick how much
12
- of them the user sees - from answers only to fully expanded - and let them switch at runtime.
14
+ of them the user sees - only the current step, icons, chips, a collapsible group, single lines
15
+ or everything - and let them switch at runtime.
13
16
 
14
17
  ![The same steps as icons, chips and single lines](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/details.png)
15
18
 
16
- **Clarifying questions.** The agent asks, the user picks one or several options or answers
17
- freely. Answered questions stay in the transcript as a record.
19
+ **Pending actions.** The agent waits for input: a generic card with dismiss, or the host's own
20
+ rendering per action owner. Resolved actions stay in the transcript as a record.
18
21
 
19
22
  **Multi-party conversations.** Colored bubbles with labels for agents that talk to each other.
20
23
 
21
24
  <p>
22
- <img src="docs/images/questions.png" alt="Clarifying questions with single and multiple choice" width="49%">
25
+ <img src="docs/images/questions.png" alt="Pending actions: a host-rendered choice and the generic card" width="49%">
23
26
  <img src="docs/images/party.png" alt="Planner and reviewer as labeled bubbles" width="49%">
24
27
  </p>
25
28
 
26
- **Your look, not a fork.** Every color, radius and font is a `--qsl-*` token. A theme is a class
27
- that overrides tokens; dark mode follows the system or `data-theme`.
29
+ **Your look, not a fork.** Override `--qsl-*` variables on any wrapper, switch light and dark
30
+ with `data-theme`, or hand quassel your own Button, Toggle, Card and Popover components.
28
31
 
29
32
  ![Default, sunset and emerald themes](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/themes.png)
30
33
 
@@ -32,11 +35,12 @@ that overrides tokens; dark mode follows the system or `data-theme`.
32
35
 
33
36
  ## Highlights
34
37
 
35
- - Streaming-safe Markdown with tables and code, no dependencies, no `dangerouslySetInnerHTML`.
36
- - Steering: typing while a run is active sends into the running turn.
37
- - Timestamps, day separators, message actions and copy buttons for code blocks.
38
+ - Streaming-safe Markdown (tables, code with copy button, lists) via Streamdown.
39
+ - Steering: typing while a turn runs sends into it; stop, retry and failed-send recovery built in.
40
+ - Attachments by button, drag and drop or paste, with previews and size limits.
41
+ - Timestamps, day separators, message actions, configurable bubbles and appearance.
38
42
  - Screen reader friendly: the transcript is not a live region; finished replies and new
39
- questions are announced once, with the host in control of the wording.
43
+ actions are announced once, with the host in control of the wording.
40
44
  - All labels are replaceable through `texts` (defaults are German).
41
45
 
42
46
  ## Install
@@ -47,36 +51,32 @@ npm install quassel
47
51
 
48
52
  | Import | Contents |
49
53
  |---|---|
50
- | `quassel` | React components, `useChat`, types |
51
- | `quassel/events` | The event contract and the pure reducer `applyEvent`, without React |
52
- | `quassel/server` | Node backend for OpenAI-compatible endpoints with sessions and SSE |
53
- | `quassel/chat.css` | Component styles including the tokens |
54
- | `quassel/foundation.css` | Tokens, color wash and glass panel for the whole page |
54
+ | `quassel` | React components, `QuasselProvider`, types, texts, `announce`, `markdownPlainText` |
55
+ | `quassel/events` | The event contract, the pure reducer `applyEvent` and the attachment limits, without React |
56
+ | `quassel/chat.css` | The compiled stylesheet: theme variables, scoped reset and all utilities quassel uses |
55
57
 
56
58
  ```tsx
57
- import { ChatInputToolbar, ChatMessages, ChatPanel, useChat } from "quassel";
59
+ import { useReducer, useState } from "react";
60
+ import { applyEvent, ChatInputToolbar, ChatMessages, ChatPanel, type Message } from "quassel";
58
61
  import "quassel/chat.css";
59
62
 
60
- export function Chat() {
61
- const { messages, running, send, stop } = useChat("http://localhost:3300/chat/demo");
63
+ export function Chat({ send, stop }: { send: (text: string) => Promise<void>; stop: () => void }) {
64
+ const [messages, dispatch] = useReducer(applyEvent, [] as Message[]);
65
+ const [running, setRunning] = useState(false);
66
+ // feed your backend's ChatEvent stream into dispatch(event) and setRunning(event.running) on "status"
62
67
  return (
63
- <ChatPanel composer={<ChatInputToolbar onSend={send} onStop={stop} running={running} />}>
68
+ <ChatPanel composer={<ChatInputToolbar onSend={send} onStop={stop} running={running} rows={1} />}>
64
69
  <ChatMessages messages={messages} running={running} detailMode="grouped" />
65
70
  </ChatPanel>
66
71
  );
67
72
  }
68
73
  ```
69
74
 
70
- ```ts
71
- import { createServer } from "node:http";
72
- import { createChatHandler, SessionManager } from "quassel/server";
73
-
74
- const manager = new SessionManager({ baseUrl: "http://localhost:11434/v1", model: "qwen3:4b" });
75
- const handler = createChatHandler({ manager });
76
-
77
- createServer(async (req, res) => {
78
- if (!(await handler(req, res))) res.writeHead(404).end();
79
- }).listen(3300);
75
+ ```css
76
+ .my-chat {
77
+ --qsl-primary: #11855c;
78
+ --qsl-radius: 0.9rem;
79
+ }
80
80
  ```
81
81
 
82
82
  Any backend that emits the `quassel/events` stream works with the same components.
@@ -85,14 +85,15 @@ Any backend that emits the `quassel/events` stream works with the same component
85
85
 
86
86
  ```sh
87
87
  pnpm install
88
- pnpm dev # gallery on http://localhost:3210
89
- pnpm check # type check
90
- pnpm release # build and publish to npm (see docs/guide.md)
88
+ pnpm dev # stylesheet in watch mode plus the gallery on http://localhost:3210
89
+ pnpm check # type check package and gallery
90
+ pnpm build # stylesheet and gallery build
91
+ pnpm release --dry-run
91
92
  ```
92
93
 
93
- The full build guide - every prop, the event contract, the backend - is in
94
- [docs/guide.md](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md) (German). It is written so that an AI assistant can build an
95
- application with quassel from it.
94
+ The full guide - every prop, the event contract, theming, slots, dark mode and publishing -
95
+ is in [docs/guide.md](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md) (German). It is written so that an AI assistant can build
96
+ an application with quassel from it.
96
97
 
97
98
  ## License
98
99
 
package/StepPopover.d.ts CHANGED
@@ -9,4 +9,4 @@ export declare function StepPopover({ message, position, texts, toolArgumentsTex
9
9
  texts: ChatTexts;
10
10
  toolArgumentsText: (tool: NonNullable<Message["tool"]>) => string;
11
11
  onClose: () => void;
12
- }): import("react").ReactPortal;
12
+ }): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
- import { ChatTexts } from "./texts";
2
- export declare function TimestampSwitch({ showTimestamps, onChange, collapsible, texts, className, }: {
1
+ import { type ChatTexts } from "./texts";
2
+ export declare function TimestampSwitch({ showTimestamps, onChange, collapsible, texts, className }: {
3
3
  showTimestamps: boolean;
4
4
  onChange: (showTimestamps: boolean) => void;
5
5
  collapsible?: boolean;
@@ -1,4 +1,6 @@
1
1
  export interface WorkingScenesProps {
2
+ /** Limits scenes to one or two visible characters for small status fields. */
3
+ compact?: boolean;
2
4
  /** Text fuer Screenreader, Default "Arbeitet ...". */
3
5
  label?: string;
4
6
  }
@@ -7,4 +9,4 @@ export interface WorkingScenesProps {
7
9
  * und Startphase, damit dieselben Szenen nie wie eine Schleife wirken. Ueber die
8
10
  * working-Prop von ChatMessages laesst er sich komplett ersetzen.
9
11
  */
10
- export declare function WorkingScenes({ label }: WorkingScenesProps): import("react").JSX.Element;
12
+ export declare function WorkingScenes({ label, compact }: WorkingScenesProps): import("react").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import type { ChatAttachmentInput } from "./events";
2
+ import { MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES } from "./chat-attachments";
3
+ import { type ChatTexts } from "./texts";
4
+ export { MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES };
5
+ export declare const maxAttachmentSizeLabel = "20 MiB";
6
+ export declare function attachmentMediaType(file: {
7
+ name: string;
8
+ type: string;
9
+ }): string;
10
+ export declare function attachmentCapabilityError(files: readonly {
11
+ name: string;
12
+ mediaType: string;
13
+ }[], capabilities: {
14
+ input: readonly string[];
15
+ model: string;
16
+ }, texts?: ChatTexts): string | undefined;
17
+ export declare function validateAttachmentSelection(existing: readonly {
18
+ size: number;
19
+ }[], incoming: readonly {
20
+ size: number;
21
+ }[], texts?: ChatTexts): void;
22
+ export declare function encodeAttachment(file: File): Promise<ChatAttachmentInput>;
23
+ export declare function formatAttachmentSize(bytes: number): string;
24
+ export declare function attachmentDownloadUrl(url: string): string;
@@ -0,0 +1,6 @@
1
+ import type { ChatAttachmentInput } from "./events";
2
+ export declare const MAX_CHAT_ATTACHMENTS = 8;
3
+ export declare const MAX_CHAT_ATTACHMENT_BYTES: number;
4
+ export declare const MAX_CHAT_REQUEST_BYTES: number;
5
+ export declare const chatAttachmentMediaType: (name: string, mediaType: string) => string;
6
+ export declare const parseChatAttachments: (value: unknown) => ChatAttachmentInput[];
@@ -0,0 +1,12 @@
1
+ interface ScrollViewport {
2
+ scrollTop: number;
3
+ readonly scrollHeight: number;
4
+ readonly clientHeight: number;
5
+ }
6
+ export declare function createChatScroll(onFollowChange?: (following: boolean) => void): {
7
+ layout: (viewport: ScrollViewport, dragging?: boolean) => void;
8
+ pause(viewport: ScrollViewport): void;
9
+ scroll(viewport: ScrollViewport, dragging?: boolean): void;
10
+ jump(viewport: ScrollViewport): void;
11
+ };
12
+ export {};