@data-club/ai-hub 0.0.11 → 0.0.12

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 (47) hide show
  1. package/dist/auth/locales/cs.d.ts.map +1 -1
  2. package/dist/auth/locales/cs.js +4 -0
  3. package/dist/auth/locales/cs.js.map +1 -1
  4. package/dist/auth/locales/sk.d.ts.map +1 -1
  5. package/dist/auth/locales/sk.js +4 -0
  6. package/dist/auth/locales/sk.js.map +1 -1
  7. package/dist/auth/texts.d.ts +8 -0
  8. package/dist/auth/texts.d.ts.map +1 -1
  9. package/dist/auth/texts.js +4 -0
  10. package/dist/auth/texts.js.map +1 -1
  11. package/dist/components/ChatShell/index.d.ts +2 -0
  12. package/dist/components/ChatShell/index.d.ts.map +1 -1
  13. package/dist/components/ChatShell/index.js +3 -1
  14. package/dist/components/ChatShell/index.js.map +1 -1
  15. package/dist/components/DataClubAIHub/index.d.ts +2 -0
  16. package/dist/components/DataClubAIHub/index.d.ts.map +1 -1
  17. package/dist/components/DataClubAIHub/index.js +2 -0
  18. package/dist/components/DataClubAIHub/index.js.map +1 -1
  19. package/dist/components/DataClubAIHubAdmin/SettingsPage.d.ts.map +1 -1
  20. package/dist/components/DataClubAIHubAdmin/SettingsPage.js +3 -1
  21. package/dist/components/DataClubAIHubAdmin/SettingsPage.js.map +1 -1
  22. package/dist/components/MainColumn/index.d.ts +8 -0
  23. package/dist/components/MainColumn/index.d.ts.map +1 -1
  24. package/dist/components/MainColumn/index.js +40 -2
  25. package/dist/components/MainColumn/index.js.map +1 -1
  26. package/dist/state/affordances.d.ts +3 -1
  27. package/dist/state/affordances.d.ts.map +1 -1
  28. package/dist/state/affordances.js +3 -1
  29. package/dist/state/affordances.js.map +1 -1
  30. package/dist/types.d.ts +2 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/util/exportConversation.d.ts +64 -0
  33. package/dist/util/exportConversation.d.ts.map +1 -0
  34. package/dist/util/exportConversation.js +78 -0
  35. package/dist/util/exportConversation.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/auth/locales/cs.ts +4 -0
  38. package/src/auth/locales/sk.ts +4 -0
  39. package/src/auth/texts.ts +12 -0
  40. package/src/components/ChatShell/index.tsx +5 -0
  41. package/src/components/DataClubAIHub/index.tsx +5 -0
  42. package/src/components/DataClubAIHubAdmin/SettingsPage.tsx +3 -1
  43. package/src/components/MainColumn/index.tsx +85 -1
  44. package/src/state/affordances.ts +5 -1
  45. package/src/styles/default.css +21 -0
  46. package/src/types.ts +2 -0
  47. package/src/util/exportConversation.ts +101 -0
@@ -67,6 +67,8 @@ export interface AIHubAffordanceFlags {
67
67
  scrollToPresentVisible: boolean;
68
68
  /** Whether the turn-status indicator renders in the chat header. Default true. */
69
69
  turnStatusVisible: boolean;
70
+ /** Whether the export-conversation (.md) button renders in the chat header. Default true. */
71
+ exportConversationEnabled: boolean;
70
72
  }
71
73
 
72
74
  /** Built-in defaults — applied when neither server nor React prop sets a value. */
@@ -84,6 +86,7 @@ export const DEFAULT_AFFORDANCE_FLAGS: AIHubAffordanceFlags = Object.freeze({
84
86
  effortControlVisible: true,
85
87
  scrollToPresentVisible: true,
86
88
  turnStatusVisible: true,
89
+ exportConversationEnabled: true,
87
90
  }) as AIHubAffordanceFlags;
88
91
 
89
92
  /**
@@ -107,6 +110,7 @@ export const FEATURE_TOGGLE_KEY_MAP: ReadonlyArray<
107
110
  ["effort_control_visible", "effortControlVisible"],
108
111
  ["scroll_to_present_visible", "scrollToPresentVisible"],
109
112
  ["turn_status_visible", "turnStatusVisible"],
113
+ ["export_conversation_enabled", "exportConversationEnabled"],
110
114
  ]);
111
115
 
112
116
  /**
@@ -134,7 +138,7 @@ function resolveOne(
134
138
  }
135
139
 
136
140
  /**
137
- * Composes the three layers into a flat resolved record of all 13
141
+ * Composes the three layers into a flat resolved record of all 14
138
142
  * affordance booleans.
139
143
  *
140
144
  * Per §8.4: server toggle wins; falling through, the React prop wins;
@@ -1492,6 +1492,27 @@
1492
1492
  white-space: nowrap;
1493
1493
  }
1494
1494
 
1495
+ .data-club-ai-hub__chat-header__export {
1496
+ padding: var(--dc-aih-space-xs) var(--dc-aih-space-sm);
1497
+ font: inherit;
1498
+ font-size: var(--dc-aih-font-size-sm);
1499
+ color: var(--dc-aih-fg-muted);
1500
+ background: transparent;
1501
+ border: 1px solid var(--dc-aih-border);
1502
+ border-radius: var(--dc-aih-radius-sm);
1503
+ white-space: nowrap;
1504
+ }
1505
+
1506
+ .data-club-ai-hub__chat-header__export:hover:not(:disabled) {
1507
+ color: var(--dc-aih-fg);
1508
+ background-color: color-mix(in srgb, var(--dc-aih-fg) 6%, transparent);
1509
+ }
1510
+
1511
+ .data-club-ai-hub__chat-header__export:disabled {
1512
+ opacity: 0.5;
1513
+ cursor: not-allowed;
1514
+ }
1515
+
1495
1516
  .data-club-ai-hub__chat-header__extras {
1496
1517
  display: flex;
1497
1518
  align-items: center;
package/src/types.ts CHANGED
@@ -716,6 +716,8 @@ export interface AIHubFeatureToggles {
716
716
  scroll_to_present_visible?: boolean;
717
717
  /** Overrides `showTurnStatus` prop. */
718
718
  turn_status_visible?: boolean;
719
+ /** Overrides `showExportButton` prop. */
720
+ export_conversation_enabled?: boolean;
719
721
  }
720
722
 
721
723
  /* ────────────────────────────────────────────────────────────────────────── */
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Conversation → Markdown export helpers.
3
+ *
4
+ * Pure functions — the `<MainColumn />` export button calls
5
+ * `buildConversationMarkdown` on the already-loaded `useAIHub().messages`
6
+ * array and hands the result to a Blob download. Keeping the serialization
7
+ * here (instead of inline in the component) makes the export rules
8
+ * unit-testable without a DOM.
9
+ *
10
+ * Export rules:
11
+ *
12
+ * - Only `user` and `assistant` messages are exported. `system` rows (the
13
+ * compiled system prompt travels in the transcript as `role: "system"`)
14
+ * and `tool` rows never appear in the file.
15
+ * - Assistant messages with no text content (tool-call-only / reasoning-only
16
+ * bubbles) are skipped — the export is the human-readable dialog, not the
17
+ * agent trace.
18
+ * - Rotating-message sessions need no special casing: the rotation runner
19
+ * deletes its own prompt's `user_message` from the Letta conversation, so
20
+ * the transcript (and therefore the export) opens with the agent's reply.
21
+ *
22
+ * @see ../components/MainColumn/index.tsx — the export button
23
+ * @see SPECIFICATIONS.md §8.4 — `export_conversation_enabled` toggle
24
+ */
25
+
26
+ import type { AIHubMessage } from "../types.js";
27
+
28
+ /** Inputs for {@link buildConversationMarkdown}. */
29
+ export interface ConversationExportArgs {
30
+ /** The session's user-supplied title; used as the document heading. */
31
+ sessionTitle: string;
32
+ /** Agent display name; rendered as the assistant speaker label. */
33
+ agentDisplayName: string;
34
+ /** Speaker label for `user` messages (localized, e.g. "You"). */
35
+ userLabel: string;
36
+ /** The transcript, chronologically sorted (as served by the provider). */
37
+ messages: ReadonlyArray<AIHubMessage>;
38
+ /** Export timestamp; injected so tests are deterministic. */
39
+ exportedAt: Date;
40
+ }
41
+
42
+ /**
43
+ * Serializes a conversation to a Markdown document.
44
+ *
45
+ * Format: an `# <title>` heading plus an "exported" byline, then one
46
+ * `## <speaker>` section per user/assistant message with the message's
47
+ * (already-markdown) content verbatim underneath.
48
+ *
49
+ * @param args - see {@link ConversationExportArgs}.
50
+ * @returns the Markdown document text (trailing newline included).
51
+ */
52
+ export function buildConversationMarkdown(args: ConversationExportArgs): string {
53
+ const lines: string[] = [];
54
+ lines.push(`# ${args.sessionTitle}`);
55
+ lines.push("");
56
+ lines.push(
57
+ `> ${args.agentDisplayName} — exported ${args.exportedAt.toISOString()}`,
58
+ );
59
+
60
+ for (const msg of args.messages) {
61
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
62
+ const content = msg.content.trim();
63
+ if (content.length === 0) continue;
64
+ const speaker =
65
+ msg.role === "user" ? args.userLabel : args.agentDisplayName;
66
+ lines.push("");
67
+ lines.push(`## ${speaker}`);
68
+ lines.push("");
69
+ lines.push(content);
70
+ }
71
+
72
+ return `${lines.join("\n")}\n`;
73
+ }
74
+
75
+ /**
76
+ * Derives a safe `.md` filename from the session title.
77
+ *
78
+ * Lowercases, strips diacritics (Czech/Slovak titles are common here),
79
+ * collapses every non-alphanumeric run to a single `-`, and appends the
80
+ * export date so repeated exports don't shadow each other in the user's
81
+ * downloads folder. Falls back to `conversation` for titles that reduce
82
+ * to nothing.
83
+ *
84
+ * @param sessionTitle - the session's title.
85
+ * @param exportedAt - export timestamp (date part is used).
86
+ * @returns e.g. `muj-rozhovor-2026-07-17.md`.
87
+ */
88
+ export function conversationExportFilename(
89
+ sessionTitle: string,
90
+ exportedAt: Date,
91
+ ): string {
92
+ const slug = sessionTitle
93
+ .normalize("NFKD")
94
+ .replace(/[\u0300-\u036f]/g, "")
95
+ .toLowerCase()
96
+ .replace(/[^a-z0-9]+/g, "-")
97
+ .replace(/^-+|-+$/g, "")
98
+ .slice(0, 60);
99
+ const date = exportedAt.toISOString().slice(0, 10);
100
+ return `${slug.length > 0 ? slug : "conversation"}-${date}.md`;
101
+ }