@refraction-ui/astro 0.8.0 → 0.9.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.
- package/dist/analytics/index.ts +5 -0
- package/dist/astro-accordion/Accordion.stories.ts +12 -0
- package/dist/astro-ai/Ai.stories.ts +12 -0
- package/dist/astro-analytics/Analytics.stories.ts +12 -0
- package/dist/astro-animated-text/AnimatedText.stories.ts +12 -0
- package/dist/astro-app-shell/AppShell.stories.ts +12 -0
- package/dist/astro-auth/Auth.stories.ts +12 -0
- package/dist/astro-avatar/Avatar.stories.ts +12 -0
- package/dist/astro-avatar-group/AvatarGroup.stories.ts +12 -0
- package/dist/astro-badge/Badge.stories.ts +16 -0
- package/dist/astro-bottom-nav/BottomNav.stories.ts +12 -0
- package/dist/astro-breadcrumbs/Breadcrumbs.stories.ts +12 -0
- package/dist/astro-button/Button.stories.ts +12 -0
- package/dist/astro-calendar/Calendar.stories.ts +12 -0
- package/dist/astro-callout/Callout.stories.ts +12 -0
- package/dist/astro-card/Card.stories.ts +12 -0
- package/dist/astro-card-grid/CardGrid.stories.ts +12 -0
- package/dist/astro-carousel/Carousel.stories.ts +12 -0
- package/dist/astro-charts/Charts.stories.ts +12 -0
- package/dist/astro-checkbox/Checkbox.stories.ts +12 -0
- package/dist/astro-code-block/CodeBlock.stories.ts +12 -0
- package/dist/astro-code-editor/CodeEditor.stories.ts +12 -0
- package/dist/astro-collapsible/Collapsible.stories.ts +12 -0
- package/dist/astro-command/Command.stories.ts +12 -0
- package/dist/astro-command-input/CommandInput.stories.ts +12 -0
- package/dist/astro-content-protection/ContentProtection.stories.ts +12 -0
- package/dist/astro-conversation/Chat.astro +208 -0
- package/dist/astro-conversation/Conversation.stories.ts +12 -0
- package/dist/astro-conversation/index.ts +29 -0
- package/dist/astro-cookie-consent/CookieConsent.astro +141 -0
- package/dist/astro-cookie-consent/index.ts +12 -0
- package/dist/astro-data-table/DataTable.stories.ts +12 -0
- package/dist/astro-date-picker/DatePicker.stories.ts +12 -0
- package/dist/astro-device-frame/DeviceFrame.stories.ts +12 -0
- package/dist/astro-dialog/Dialog.stories.ts +12 -0
- package/dist/astro-diff-viewer/DiffViewer.stories.ts +12 -0
- package/dist/astro-dropdown-menu/DropdownMenu.stories.ts +12 -0
- package/dist/astro-emoji-picker/EmojiPicker.stories.ts +12 -0
- package/dist/astro-feedback-dialog/FeedbackDialog.stories.ts +12 -0
- package/dist/astro-file-tree/FileTree.stories.ts +12 -0
- package/dist/astro-file-upload/FileUpload.stories.ts +12 -0
- package/dist/astro-footer/Footer.stories.ts +12 -0
- package/dist/astro-icon-system/IconSystem.stories.ts +12 -0
- package/dist/astro-inline-editor/InlineEditor.stories.ts +12 -0
- package/dist/astro-input/Input.stories.ts +12 -0
- package/dist/astro-input-group/InputGroup.stories.ts +12 -0
- package/dist/astro-install-prompt/InstallPrompt.stories.ts +12 -0
- package/dist/astro-keyboard-shortcut/KeyboardShortcut.stories.ts +12 -0
- package/dist/astro-language-selector/LanguageSelector.stories.ts +12 -0
- package/dist/astro-link-card/LinkCard.stories.ts +12 -0
- package/dist/astro-logger/Logger.stories.ts +12 -0
- package/dist/astro-markdown-renderer/MarkdownRenderer.stories.ts +12 -0
- package/dist/astro-mobile-nav/MobileNav.stories.ts +12 -0
- package/dist/astro-navbar/Navbar.stories.ts +12 -0
- package/dist/astro-otp-input/OtpInput.stories.ts +12 -0
- package/dist/astro-pagination/Pagination.stories.ts +12 -0
- package/dist/astro-payment/Payment.stories.ts +12 -0
- package/dist/astro-popover/Popover.stories.ts +12 -0
- package/dist/astro-presence-indicator/PresenceIndicator.stories.ts +12 -0
- package/dist/astro-progress-display/ProgressDisplay.stories.ts +12 -0
- package/dist/astro-radio/Radio.stories.ts +12 -0
- package/dist/astro-reaction-bar/ReactionBar.stories.ts +12 -0
- package/dist/astro-resizable-layout/ResizableLayout.stories.ts +12 -0
- package/dist/astro-rich-editor/RichEditor.stories.ts +12 -0
- package/dist/astro-search-bar/SearchBar.stories.ts +12 -0
- package/dist/astro-select/Select.stories.ts +12 -0
- package/dist/astro-sidebar/Sidebar.stories.ts +12 -0
- package/dist/astro-skeleton/Skeleton.stories.ts +12 -0
- package/dist/astro-skip-to-content/SkipToContent.stories.ts +12 -0
- package/dist/astro-slide-viewer/SlideViewer.stories.ts +12 -0
- package/dist/astro-status-indicator/StatusIndicator.stories.ts +12 -0
- package/dist/astro-steps/Steps.stories.ts +12 -0
- package/dist/astro-switch/Switch.stories.ts +12 -0
- package/dist/astro-table-of-contents/TableOfContents.stories.ts +12 -0
- package/dist/astro-tabs/Tabs.stories.ts +12 -0
- package/dist/astro-textarea/Textarea.stories.ts +12 -0
- package/dist/astro-theme/Theme.stories.ts +12 -0
- package/dist/astro-thread-view/ThreadView.stories.ts +12 -0
- package/dist/astro-toast/Toast.stories.ts +12 -0
- package/dist/astro-tooltip/Tooltip.stories.ts +12 -0
- package/dist/astro-version-selector/VersionSelector.stories.ts +12 -0
- package/dist/astro-video-player/VideoPlayer.stories.ts +12 -0
- package/dist/astro-voice-pill/VoicePill.stories.ts +12 -0
- package/dist/astro-waveform/Waveform.stories.ts +12 -0
- package/dist/conversation/conversation.ts +379 -0
- package/dist/conversation/index.ts +30 -0
- package/dist/conversation/selectors.ts +58 -0
- package/dist/conversation/types.ts +222 -0
- package/dist/cookie-consent/cookie-consent.ts +132 -0
- package/dist/cookie-consent/index.ts +10 -0
- package/dist/cookie-consent/types.ts +72 -0
- package/dist/index.ts +2 -0
- package/dist/shared/library-origin-error.ts +1 -2
- package/dist/shared/types.ts +1 -1
- package/dist/theme/theme-script.ts +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './SkipToContent.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/SkipToContent',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './SlideViewer.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/SlideViewer',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './StatusIndicator.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/StatusIndicator',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Steps.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Steps',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Switch.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Switch',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './TableOfContents.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/TableOfContents',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Tabs.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Tabs',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Textarea.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Textarea',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './ThemeScript.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Theme',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './ThreadView.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/ThreadView',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Toast.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Toast',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Tooltip.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Tooltip',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './VersionSelector.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/VersionSelector',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './VideoPlayer.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/VideoPlayer',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './VoicePill.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/VoicePill',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Component from './Waveform.astro'
|
|
2
|
+
|
|
3
|
+
// Auto-generated baseline story. Renders the component with empty args;
|
|
4
|
+
// components that require props show a render error (enrich by hand as needed).
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Astro/Waveform',
|
|
7
|
+
component: Component,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
|
|
12
|
+
export const Default = { args: {} }
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { generateId } from '../shared/index.ts'
|
|
2
|
+
import { rootIdOf, selectRoots, selectThreadMessages } from './selectors.js'
|
|
3
|
+
import type {
|
|
4
|
+
ChatAuthor,
|
|
5
|
+
ChatMessage,
|
|
6
|
+
Conversation,
|
|
7
|
+
ConversationAPI,
|
|
8
|
+
ConversationConfig,
|
|
9
|
+
ConversationState,
|
|
10
|
+
SendOptions,
|
|
11
|
+
ThreadingMode,
|
|
12
|
+
} from './types.js'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_ASSISTANT: ChatAuthor = { id: 'assistant', name: 'Assistant' }
|
|
15
|
+
const DEFAULT_USER: ChatAuthor = { id: 'user', name: 'You' }
|
|
16
|
+
const DEFAULT_TITLE = 'New conversation'
|
|
17
|
+
|
|
18
|
+
/** Default title: first line, trimmed to ~48 chars. */
|
|
19
|
+
function defaultGenerateTitle(firstMessage: string): string {
|
|
20
|
+
const firstLine = firstMessage.split('\n', 1)[0]!.trim()
|
|
21
|
+
if (firstLine.length <= 48) return firstLine || DEFAULT_TITLE
|
|
22
|
+
return `${firstLine.slice(0, 47).trimEnd()}…`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* createConversation — headless multi-conversation chat store with reply-threads,
|
|
27
|
+
* reactions, edit/delete, and streaming. Backend-agnostic: provide a
|
|
28
|
+
* {@link ChatTransport} and `sendMessage` will optimistically append the user
|
|
29
|
+
* message, stream the reply, and expose retry/stop. Without a transport it is a
|
|
30
|
+
* pure local message log.
|
|
31
|
+
*/
|
|
32
|
+
export function createConversation(config: ConversationConfig = {}): ConversationAPI {
|
|
33
|
+
const assistant = config.assistant ?? DEFAULT_ASSISTANT
|
|
34
|
+
const currentUser = config.currentUser ?? DEFAULT_USER
|
|
35
|
+
const generateTitle = config.generateTitle ?? defaultGenerateTitle
|
|
36
|
+
|
|
37
|
+
let transport = config.transport
|
|
38
|
+
let threadingMode: ThreadingMode = config.threadingMode ?? 'inline'
|
|
39
|
+
const conversations = new Map<string, Conversation>()
|
|
40
|
+
const messagesByConv = new Map<string, ChatMessage[]>()
|
|
41
|
+
let activeConversationId: string | null = config.activeConversationId ?? null
|
|
42
|
+
let openThreadRootId: string | null = null
|
|
43
|
+
let status: ConversationState['status'] = 'idle'
|
|
44
|
+
let error: string | null = null
|
|
45
|
+
let abortController: AbortController | null = null
|
|
46
|
+
|
|
47
|
+
// Seed
|
|
48
|
+
for (const c of config.conversations ?? []) {
|
|
49
|
+
conversations.set(c.id, c)
|
|
50
|
+
messagesByConv.set(c.id, [])
|
|
51
|
+
}
|
|
52
|
+
for (const [conversationId, msgs] of Object.entries(config.messages ?? {})) {
|
|
53
|
+
messagesByConv.set(conversationId, [...msgs])
|
|
54
|
+
}
|
|
55
|
+
if (activeConversationId === null && config.conversations?.length) {
|
|
56
|
+
activeConversationId = config.conversations[0]!.id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// — subscription —
|
|
60
|
+
const listeners = new Set<() => void>()
|
|
61
|
+
let snapshot: ConversationState = buildSnapshot()
|
|
62
|
+
|
|
63
|
+
function buildSnapshot(): ConversationState {
|
|
64
|
+
return {
|
|
65
|
+
conversations: orderedConversations(),
|
|
66
|
+
activeConversationId,
|
|
67
|
+
messages: activeConversationId ? (messagesByConv.get(activeConversationId) ?? []) : [],
|
|
68
|
+
openThreadRootId,
|
|
69
|
+
threadingMode,
|
|
70
|
+
status,
|
|
71
|
+
error,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function emit(): void {
|
|
76
|
+
snapshot = buildSnapshot()
|
|
77
|
+
for (const l of listeners) l()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function orderedConversations(): Conversation[] {
|
|
81
|
+
return [...conversations.values()].sort(
|
|
82
|
+
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function touch(conversationId: string): void {
|
|
87
|
+
const c = conversations.get(conversationId)
|
|
88
|
+
if (c) conversations.set(conversationId, { ...c, updatedAt: new Date() })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function ensureActiveConversation(opts?: SendOptions): string {
|
|
92
|
+
if (opts?.conversationId) {
|
|
93
|
+
if (!conversations.has(opts.conversationId)) createConversationInternal({}, opts.conversationId)
|
|
94
|
+
return opts.conversationId
|
|
95
|
+
}
|
|
96
|
+
if (activeConversationId && conversations.has(activeConversationId)) return activeConversationId
|
|
97
|
+
return createConversationInternal({}).id
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createConversationInternal(
|
|
101
|
+
opts: { title?: string; metadata?: Record<string, unknown> },
|
|
102
|
+
id?: string,
|
|
103
|
+
): Conversation {
|
|
104
|
+
const now = new Date()
|
|
105
|
+
const conversation: Conversation = {
|
|
106
|
+
id: id ?? generateId('rfr-conv'),
|
|
107
|
+
title: opts.title ?? DEFAULT_TITLE,
|
|
108
|
+
createdAt: now,
|
|
109
|
+
updatedAt: now,
|
|
110
|
+
metadata: opts.metadata,
|
|
111
|
+
}
|
|
112
|
+
conversations.set(conversation.id, conversation)
|
|
113
|
+
messagesByConv.set(conversation.id, [])
|
|
114
|
+
activeConversationId = conversation.id
|
|
115
|
+
return conversation
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Run a transport stream into the given assistant message. */
|
|
119
|
+
async function streamReply(
|
|
120
|
+
conversationId: string,
|
|
121
|
+
assistantMsg: ChatMessage,
|
|
122
|
+
userMsg: ChatMessage,
|
|
123
|
+
history: ChatMessage[],
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
abortController = new AbortController()
|
|
126
|
+
status = 'streaming'
|
|
127
|
+
emit()
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const stream = transport!.send({
|
|
131
|
+
conversationId,
|
|
132
|
+
message: userMsg,
|
|
133
|
+
history,
|
|
134
|
+
parentId: assistantMsg.parentId,
|
|
135
|
+
signal: abortController.signal,
|
|
136
|
+
})
|
|
137
|
+
for await (const chunk of stream) {
|
|
138
|
+
// Apply the delivered chunk before honoring an abort, so a `stop()` mid
|
|
139
|
+
// stream keeps the partial reply rather than discarding the last token.
|
|
140
|
+
if (chunk.content !== undefined) assistantMsg.content = chunk.content
|
|
141
|
+
if (chunk.delta) assistantMsg.content += chunk.delta
|
|
142
|
+
if (chunk.metadata) {
|
|
143
|
+
assistantMsg.metadata = { ...assistantMsg.metadata, ...chunk.metadata }
|
|
144
|
+
}
|
|
145
|
+
emit()
|
|
146
|
+
if (abortController.signal.aborted) break
|
|
147
|
+
}
|
|
148
|
+
assistantMsg.status = 'sent'
|
|
149
|
+
status = 'idle'
|
|
150
|
+
error = null
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (abortController.signal.aborted) {
|
|
153
|
+
assistantMsg.status = 'sent'
|
|
154
|
+
status = 'idle'
|
|
155
|
+
} else {
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
157
|
+
assistantMsg.status = 'error'
|
|
158
|
+
assistantMsg.error = message
|
|
159
|
+
status = 'error'
|
|
160
|
+
error = message
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
abortController = null
|
|
164
|
+
touch(conversationId)
|
|
165
|
+
emit()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** History the transport sees: the thread when replying, else the main timeline. */
|
|
170
|
+
function historyFor(list: ChatMessage[], parentId: string | undefined): ChatMessage[] {
|
|
171
|
+
const scope = parentId
|
|
172
|
+
? selectThreadMessages(list, parentId)
|
|
173
|
+
: selectRoots(list)
|
|
174
|
+
return scope.filter((m) => m.status !== 'error')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const api: ConversationAPI = {
|
|
178
|
+
getState() {
|
|
179
|
+
return snapshot
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
subscribe(listener) {
|
|
183
|
+
listeners.add(listener)
|
|
184
|
+
return () => listeners.delete(listener)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
newConversation(opts) {
|
|
188
|
+
const conversation = createConversationInternal(opts ?? {})
|
|
189
|
+
openThreadRootId = null
|
|
190
|
+
emit()
|
|
191
|
+
return conversation
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
selectConversation(conversationId) {
|
|
195
|
+
if (!conversations.has(conversationId) || activeConversationId === conversationId) return
|
|
196
|
+
activeConversationId = conversationId
|
|
197
|
+
openThreadRootId = null
|
|
198
|
+
emit()
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
deleteConversation(conversationId) {
|
|
202
|
+
if (!conversations.has(conversationId)) return
|
|
203
|
+
conversations.delete(conversationId)
|
|
204
|
+
messagesByConv.delete(conversationId)
|
|
205
|
+
if (activeConversationId === conversationId) {
|
|
206
|
+
activeConversationId = orderedConversations()[0]?.id ?? null
|
|
207
|
+
openThreadRootId = null
|
|
208
|
+
}
|
|
209
|
+
emit()
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
renameConversation(conversationId, title) {
|
|
213
|
+
const c = conversations.get(conversationId)
|
|
214
|
+
if (!c) return
|
|
215
|
+
conversations.set(conversationId, { ...c, title })
|
|
216
|
+
emit()
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
appendMessage(message) {
|
|
220
|
+
const list = messagesByConv.get(message.conversationId)
|
|
221
|
+
if (!list) return
|
|
222
|
+
list.push(message)
|
|
223
|
+
touch(message.conversationId)
|
|
224
|
+
emit()
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
editMessage(messageId, content) {
|
|
228
|
+
if (!activeConversationId) return
|
|
229
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
230
|
+
const msg = list.find((m) => m.id === messageId)
|
|
231
|
+
if (!msg) return
|
|
232
|
+
msg.content = content
|
|
233
|
+
msg.edited = true
|
|
234
|
+
emit()
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
deleteMessage(messageId) {
|
|
238
|
+
if (!activeConversationId) return
|
|
239
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
240
|
+
const msg = list.find((m) => m.id === messageId)
|
|
241
|
+
if (!msg) return
|
|
242
|
+
const removeIds = new Set<string>([messageId])
|
|
243
|
+
// Deleting a thread root removes its replies too.
|
|
244
|
+
if (!msg.parentId) for (const r of list) if (r.parentId === messageId) removeIds.add(r.id)
|
|
245
|
+
const next = list.filter((m) => !removeIds.has(m.id))
|
|
246
|
+
messagesByConv.set(activeConversationId, next)
|
|
247
|
+
if (openThreadRootId && removeIds.has(openThreadRootId)) openThreadRootId = null
|
|
248
|
+
emit()
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
react(messageId, emoji) {
|
|
252
|
+
if (!activeConversationId) return
|
|
253
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
254
|
+
const msg = list.find((m) => m.id === messageId)
|
|
255
|
+
if (!msg) return
|
|
256
|
+
const reactions = [...(msg.reactions ?? [])]
|
|
257
|
+
const idx = reactions.findIndex((r) => r.emoji === emoji)
|
|
258
|
+
if (idx === -1) {
|
|
259
|
+
reactions.push({ emoji, count: 1, userReacted: true })
|
|
260
|
+
} else {
|
|
261
|
+
const r = reactions[idx]!
|
|
262
|
+
if (r.userReacted) {
|
|
263
|
+
const count = r.count - 1
|
|
264
|
+
if (count <= 0) reactions.splice(idx, 1)
|
|
265
|
+
else reactions[idx] = { ...r, count, userReacted: false }
|
|
266
|
+
} else {
|
|
267
|
+
reactions[idx] = { ...r, count: r.count + 1, userReacted: true }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
msg.reactions = reactions
|
|
271
|
+
emit()
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async sendMessage(content, opts) {
|
|
275
|
+
const trimmed = content.trim()
|
|
276
|
+
if (!trimmed && !opts?.attachments?.length) return
|
|
277
|
+
|
|
278
|
+
const conversationId = ensureActiveConversation(opts)
|
|
279
|
+
const list = messagesByConv.get(conversationId)!
|
|
280
|
+
const parentId = opts?.replyTo ? rootIdOf(list, opts.replyTo) : undefined
|
|
281
|
+
const isFirstRoot = !parentId && selectRoots(list).length === 0
|
|
282
|
+
|
|
283
|
+
const userMsg: ChatMessage = {
|
|
284
|
+
id: generateId('rfr-msg'),
|
|
285
|
+
conversationId,
|
|
286
|
+
role: 'user',
|
|
287
|
+
author: currentUser,
|
|
288
|
+
content: trimmed,
|
|
289
|
+
timestamp: new Date(),
|
|
290
|
+
status: 'sent',
|
|
291
|
+
parentId,
|
|
292
|
+
attachments: opts?.attachments,
|
|
293
|
+
metadata: opts?.metadata,
|
|
294
|
+
}
|
|
295
|
+
list.push(userMsg)
|
|
296
|
+
|
|
297
|
+
const conversation = conversations.get(conversationId)!
|
|
298
|
+
if (isFirstRoot && conversation.title === DEFAULT_TITLE) {
|
|
299
|
+
conversations.set(conversationId, { ...conversation, title: generateTitle(trimmed) })
|
|
300
|
+
}
|
|
301
|
+
touch(conversationId)
|
|
302
|
+
|
|
303
|
+
if (!transport) {
|
|
304
|
+
status = 'idle'
|
|
305
|
+
emit()
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// history excludes the just-pushed user message
|
|
310
|
+
const history = historyFor(list, parentId).filter((m) => m.id !== userMsg.id)
|
|
311
|
+
const assistantMsg: ChatMessage = {
|
|
312
|
+
id: generateId('rfr-msg'),
|
|
313
|
+
conversationId,
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
author: assistant,
|
|
316
|
+
content: '',
|
|
317
|
+
timestamp: new Date(),
|
|
318
|
+
status: 'streaming',
|
|
319
|
+
parentId,
|
|
320
|
+
}
|
|
321
|
+
list.push(assistantMsg)
|
|
322
|
+
await streamReply(conversationId, assistantMsg, userMsg, history)
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
async retryLast() {
|
|
326
|
+
if (!activeConversationId || !transport) return
|
|
327
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
328
|
+
const last = list[list.length - 1]
|
|
329
|
+
const parentId = last?.parentId
|
|
330
|
+
// Drop a trailing failed assistant message, if any.
|
|
331
|
+
if (last && last.role === 'assistant' && last.status === 'error') list.pop()
|
|
332
|
+
|
|
333
|
+
const scope = parentId ? selectThreadMessages(list, parentId) : selectRoots(list)
|
|
334
|
+
const lastUser = [...scope].reverse().find((m) => m.role === 'user')
|
|
335
|
+
if (!lastUser) return
|
|
336
|
+
|
|
337
|
+
const history = historyFor(list, parentId)
|
|
338
|
+
const assistantMsg: ChatMessage = {
|
|
339
|
+
id: generateId('rfr-msg'),
|
|
340
|
+
conversationId: activeConversationId,
|
|
341
|
+
role: 'assistant',
|
|
342
|
+
author: assistant,
|
|
343
|
+
content: '',
|
|
344
|
+
timestamp: new Date(),
|
|
345
|
+
status: 'streaming',
|
|
346
|
+
parentId,
|
|
347
|
+
}
|
|
348
|
+
list.push(assistantMsg)
|
|
349
|
+
await streamReply(activeConversationId, assistantMsg, lastUser, history)
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
stop() {
|
|
353
|
+
abortController?.abort()
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
openThread(rootId) {
|
|
357
|
+
openThreadRootId = rootId
|
|
358
|
+
emit()
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
closeThread() {
|
|
362
|
+
if (openThreadRootId === null) return
|
|
363
|
+
openThreadRootId = null
|
|
364
|
+
emit()
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
setThreadingMode(mode) {
|
|
368
|
+
if (threadingMode === mode) return
|
|
369
|
+
threadingMode = mode
|
|
370
|
+
emit()
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
setTransport(next) {
|
|
374
|
+
transport = next
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return api
|
|
379
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { createConversation } from './conversation.js'
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
findMessage,
|
|
5
|
+
rootIdOf,
|
|
6
|
+
selectRoots,
|
|
7
|
+
selectReplies,
|
|
8
|
+
selectThreadMessages,
|
|
9
|
+
getReplyCount,
|
|
10
|
+
selectMainTimeline,
|
|
11
|
+
selectTimelineFromState,
|
|
12
|
+
} from './selectors.js'
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
ChatAuthor,
|
|
16
|
+
ChatRole,
|
|
17
|
+
MessageStatus,
|
|
18
|
+
ThreadingMode,
|
|
19
|
+
MessageReaction,
|
|
20
|
+
MessageAttachment,
|
|
21
|
+
ChatMessage,
|
|
22
|
+
Conversation,
|
|
23
|
+
SendContext,
|
|
24
|
+
TransportChunk,
|
|
25
|
+
ChatTransport,
|
|
26
|
+
ConversationState,
|
|
27
|
+
ConversationConfig,
|
|
28
|
+
SendOptions,
|
|
29
|
+
ConversationAPI,
|
|
30
|
+
} from './types.js'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ChatMessage, ConversationState, ThreadingMode } from './types.js'
|
|
2
|
+
|
|
3
|
+
/** Sort by timestamp ascending (oldest first). */
|
|
4
|
+
function byTime(a: ChatMessage, b: ChatMessage): number {
|
|
5
|
+
return a.timestamp.getTime() - b.timestamp.getTime()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Find a message by id. */
|
|
9
|
+
export function findMessage(messages: ChatMessage[], id: string): ChatMessage | undefined {
|
|
10
|
+
return messages.find((m) => m.id === id)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve a message id to its thread root (itself if it's already a root). */
|
|
14
|
+
export function rootIdOf(messages: ChatMessage[], id: string): string {
|
|
15
|
+
const m = findMessage(messages, id)
|
|
16
|
+
if (!m) return id
|
|
17
|
+
return m.parentId ?? m.id
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Root (main-timeline) messages — those not belonging to a reply-thread. */
|
|
21
|
+
export function selectRoots(messages: ChatMessage[]): ChatMessage[] {
|
|
22
|
+
return messages.filter((m) => !m.parentId).sort(byTime)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Replies belonging to a thread root, oldest first (excludes the root itself). */
|
|
26
|
+
export function selectReplies(messages: ChatMessage[], rootId: string): ChatMessage[] {
|
|
27
|
+
return messages.filter((m) => m.parentId === rootId).sort(byTime)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A full thread: root message followed by its replies. */
|
|
31
|
+
export function selectThreadMessages(messages: ChatMessage[], rootId: string): ChatMessage[] {
|
|
32
|
+
const root = findMessage(messages, rootId)
|
|
33
|
+
const replies = selectReplies(messages, rootId)
|
|
34
|
+
return root ? [root, ...replies] : replies
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Number of replies in a thread. */
|
|
38
|
+
export function getReplyCount(messages: ChatMessage[], rootId: string): number {
|
|
39
|
+
return messages.reduce((n, m) => (m.parentId === rootId ? n + 1 : n), 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The main timeline for the active conversation, honoring the threading mode:
|
|
44
|
+
* - `'inline'`: every message in chronological order (replies included).
|
|
45
|
+
* - `'panel'`: only root messages (replies hidden behind their thread).
|
|
46
|
+
*/
|
|
47
|
+
export function selectMainTimeline(
|
|
48
|
+
messages: ChatMessage[],
|
|
49
|
+
mode: ThreadingMode,
|
|
50
|
+
): ChatMessage[] {
|
|
51
|
+
if (mode === 'panel') return selectRoots(messages)
|
|
52
|
+
return [...messages].sort(byTime)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Convenience: main timeline from a store snapshot. */
|
|
56
|
+
export function selectTimelineFromState(state: ConversationState): ChatMessage[] {
|
|
57
|
+
return selectMainTimeline(state.messages, state.threadingMode)
|
|
58
|
+
}
|