@refraction-ui/astro 0.8.1 → 0.10.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 +399 -0
- package/dist/conversation/index.ts +30 -0
- package/dist/conversation/selectors.ts +58 -0
- package/dist/conversation/types.ts +232 -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/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,399 @@
|
|
|
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 replyTarget: string | null = null
|
|
44
|
+
let status: ConversationState['status'] = 'idle'
|
|
45
|
+
let error: string | null = null
|
|
46
|
+
let abortController: AbortController | null = null
|
|
47
|
+
|
|
48
|
+
// Seed
|
|
49
|
+
for (const c of config.conversations ?? []) {
|
|
50
|
+
conversations.set(c.id, c)
|
|
51
|
+
messagesByConv.set(c.id, [])
|
|
52
|
+
}
|
|
53
|
+
for (const [conversationId, msgs] of Object.entries(config.messages ?? {})) {
|
|
54
|
+
messagesByConv.set(conversationId, [...msgs])
|
|
55
|
+
}
|
|
56
|
+
if (activeConversationId === null && config.conversations?.length) {
|
|
57
|
+
activeConversationId = config.conversations[0]!.id
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// — subscription —
|
|
61
|
+
const listeners = new Set<() => void>()
|
|
62
|
+
let snapshot: ConversationState = buildSnapshot()
|
|
63
|
+
|
|
64
|
+
function buildSnapshot(): ConversationState {
|
|
65
|
+
return {
|
|
66
|
+
conversations: orderedConversations(),
|
|
67
|
+
activeConversationId,
|
|
68
|
+
messages: activeConversationId ? (messagesByConv.get(activeConversationId) ?? []) : [],
|
|
69
|
+
openThreadRootId,
|
|
70
|
+
replyTarget,
|
|
71
|
+
threadingMode,
|
|
72
|
+
status,
|
|
73
|
+
error,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function emit(): void {
|
|
78
|
+
snapshot = buildSnapshot()
|
|
79
|
+
for (const l of listeners) l()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function orderedConversations(): Conversation[] {
|
|
83
|
+
return [...conversations.values()].sort(
|
|
84
|
+
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function touch(conversationId: string): void {
|
|
89
|
+
const c = conversations.get(conversationId)
|
|
90
|
+
if (c) conversations.set(conversationId, { ...c, updatedAt: new Date() })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ensureActiveConversation(opts?: SendOptions): string {
|
|
94
|
+
if (opts?.conversationId) {
|
|
95
|
+
if (!conversations.has(opts.conversationId)) createConversationInternal({}, opts.conversationId)
|
|
96
|
+
return opts.conversationId
|
|
97
|
+
}
|
|
98
|
+
if (activeConversationId && conversations.has(activeConversationId)) return activeConversationId
|
|
99
|
+
return createConversationInternal({}).id
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createConversationInternal(
|
|
103
|
+
opts: { title?: string; metadata?: Record<string, unknown> },
|
|
104
|
+
id?: string,
|
|
105
|
+
): Conversation {
|
|
106
|
+
const now = new Date()
|
|
107
|
+
const conversation: Conversation = {
|
|
108
|
+
id: id ?? generateId('rfr-conv'),
|
|
109
|
+
title: opts.title ?? DEFAULT_TITLE,
|
|
110
|
+
createdAt: now,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
metadata: opts.metadata,
|
|
113
|
+
}
|
|
114
|
+
conversations.set(conversation.id, conversation)
|
|
115
|
+
messagesByConv.set(conversation.id, [])
|
|
116
|
+
activeConversationId = conversation.id
|
|
117
|
+
return conversation
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Run a transport stream into the given assistant message. */
|
|
121
|
+
async function streamReply(
|
|
122
|
+
conversationId: string,
|
|
123
|
+
assistantMsg: ChatMessage,
|
|
124
|
+
userMsg: ChatMessage,
|
|
125
|
+
history: ChatMessage[],
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
abortController = new AbortController()
|
|
128
|
+
status = 'streaming'
|
|
129
|
+
emit()
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const stream = transport!.send({
|
|
133
|
+
conversationId,
|
|
134
|
+
message: userMsg,
|
|
135
|
+
history,
|
|
136
|
+
parentId: assistantMsg.parentId,
|
|
137
|
+
signal: abortController.signal,
|
|
138
|
+
})
|
|
139
|
+
for await (const chunk of stream) {
|
|
140
|
+
// Apply the delivered chunk before honoring an abort, so a `stop()` mid
|
|
141
|
+
// stream keeps the partial reply rather than discarding the last token.
|
|
142
|
+
if (chunk.content !== undefined) assistantMsg.content = chunk.content
|
|
143
|
+
if (chunk.delta) assistantMsg.content += chunk.delta
|
|
144
|
+
if (chunk.metadata) {
|
|
145
|
+
assistantMsg.metadata = { ...assistantMsg.metadata, ...chunk.metadata }
|
|
146
|
+
}
|
|
147
|
+
emit()
|
|
148
|
+
if (abortController.signal.aborted) break
|
|
149
|
+
}
|
|
150
|
+
assistantMsg.status = 'sent'
|
|
151
|
+
status = 'idle'
|
|
152
|
+
error = null
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (abortController.signal.aborted) {
|
|
155
|
+
assistantMsg.status = 'sent'
|
|
156
|
+
status = 'idle'
|
|
157
|
+
} else {
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
159
|
+
assistantMsg.status = 'error'
|
|
160
|
+
assistantMsg.error = message
|
|
161
|
+
status = 'error'
|
|
162
|
+
error = message
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
abortController = null
|
|
166
|
+
touch(conversationId)
|
|
167
|
+
emit()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** History the transport sees: the thread when replying, else the main timeline. */
|
|
172
|
+
function historyFor(list: ChatMessage[], parentId: string | undefined): ChatMessage[] {
|
|
173
|
+
const scope = parentId
|
|
174
|
+
? selectThreadMessages(list, parentId)
|
|
175
|
+
: selectRoots(list)
|
|
176
|
+
return scope.filter((m) => m.status !== 'error')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const api: ConversationAPI = {
|
|
180
|
+
getState() {
|
|
181
|
+
return snapshot
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
subscribe(listener) {
|
|
185
|
+
listeners.add(listener)
|
|
186
|
+
return () => listeners.delete(listener)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
newConversation(opts) {
|
|
190
|
+
const conversation = createConversationInternal(opts ?? {})
|
|
191
|
+
openThreadRootId = null
|
|
192
|
+
replyTarget = null
|
|
193
|
+
emit()
|
|
194
|
+
return conversation
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
selectConversation(conversationId) {
|
|
198
|
+
if (!conversations.has(conversationId) || activeConversationId === conversationId) return
|
|
199
|
+
activeConversationId = conversationId
|
|
200
|
+
openThreadRootId = null
|
|
201
|
+
replyTarget = null
|
|
202
|
+
emit()
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
deleteConversation(conversationId) {
|
|
206
|
+
if (!conversations.has(conversationId)) return
|
|
207
|
+
conversations.delete(conversationId)
|
|
208
|
+
messagesByConv.delete(conversationId)
|
|
209
|
+
if (activeConversationId === conversationId) {
|
|
210
|
+
activeConversationId = orderedConversations()[0]?.id ?? null
|
|
211
|
+
openThreadRootId = null
|
|
212
|
+
replyTarget = null
|
|
213
|
+
}
|
|
214
|
+
emit()
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
renameConversation(conversationId, title) {
|
|
218
|
+
const c = conversations.get(conversationId)
|
|
219
|
+
if (!c) return
|
|
220
|
+
conversations.set(conversationId, { ...c, title })
|
|
221
|
+
emit()
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
appendMessage(message) {
|
|
225
|
+
const list = messagesByConv.get(message.conversationId)
|
|
226
|
+
if (!list) return
|
|
227
|
+
list.push(message)
|
|
228
|
+
touch(message.conversationId)
|
|
229
|
+
emit()
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
editMessage(messageId, content) {
|
|
233
|
+
if (!activeConversationId) return
|
|
234
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
235
|
+
const msg = list.find((m) => m.id === messageId)
|
|
236
|
+
if (!msg) return
|
|
237
|
+
msg.content = content
|
|
238
|
+
msg.edited = true
|
|
239
|
+
emit()
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
deleteMessage(messageId) {
|
|
243
|
+
if (!activeConversationId) return
|
|
244
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
245
|
+
const msg = list.find((m) => m.id === messageId)
|
|
246
|
+
if (!msg) return
|
|
247
|
+
const removeIds = new Set<string>([messageId])
|
|
248
|
+
// Deleting a thread root removes its replies too.
|
|
249
|
+
if (!msg.parentId) for (const r of list) if (r.parentId === messageId) removeIds.add(r.id)
|
|
250
|
+
const next = list.filter((m) => !removeIds.has(m.id))
|
|
251
|
+
messagesByConv.set(activeConversationId, next)
|
|
252
|
+
if (openThreadRootId && removeIds.has(openThreadRootId)) openThreadRootId = null
|
|
253
|
+
if (replyTarget && removeIds.has(replyTarget)) replyTarget = openThreadRootId
|
|
254
|
+
emit()
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
react(messageId, emoji) {
|
|
258
|
+
if (!activeConversationId) return
|
|
259
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
260
|
+
const msg = list.find((m) => m.id === messageId)
|
|
261
|
+
if (!msg) return
|
|
262
|
+
const reactions = [...(msg.reactions ?? [])]
|
|
263
|
+
const idx = reactions.findIndex((r) => r.emoji === emoji)
|
|
264
|
+
if (idx === -1) {
|
|
265
|
+
reactions.push({ emoji, count: 1, userReacted: true })
|
|
266
|
+
} else {
|
|
267
|
+
const r = reactions[idx]!
|
|
268
|
+
if (r.userReacted) {
|
|
269
|
+
const count = r.count - 1
|
|
270
|
+
if (count <= 0) reactions.splice(idx, 1)
|
|
271
|
+
else reactions[idx] = { ...r, count, userReacted: false }
|
|
272
|
+
} else {
|
|
273
|
+
reactions[idx] = { ...r, count: r.count + 1, userReacted: true }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
msg.reactions = reactions
|
|
277
|
+
emit()
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
async sendMessage(content, opts) {
|
|
281
|
+
const trimmed = content.trim()
|
|
282
|
+
if (!trimmed && !opts?.attachments?.length) return
|
|
283
|
+
|
|
284
|
+
const conversationId = ensureActiveConversation(opts)
|
|
285
|
+
const list = messagesByConv.get(conversationId)!
|
|
286
|
+
// Group under the originating root, but remember the specific message replied to.
|
|
287
|
+
const parentId = opts?.replyTo ? rootIdOf(list, opts.replyTo) : undefined
|
|
288
|
+
const replyToId = opts?.replyTo
|
|
289
|
+
const isFirstRoot = !parentId && selectRoots(list).length === 0
|
|
290
|
+
|
|
291
|
+
const userMsg: ChatMessage = {
|
|
292
|
+
id: generateId('rfr-msg'),
|
|
293
|
+
conversationId,
|
|
294
|
+
role: 'user',
|
|
295
|
+
author: currentUser,
|
|
296
|
+
content: trimmed,
|
|
297
|
+
timestamp: new Date(),
|
|
298
|
+
status: 'sent',
|
|
299
|
+
parentId,
|
|
300
|
+
replyToId,
|
|
301
|
+
attachments: opts?.attachments,
|
|
302
|
+
metadata: opts?.metadata,
|
|
303
|
+
}
|
|
304
|
+
list.push(userMsg)
|
|
305
|
+
|
|
306
|
+
const conversation = conversations.get(conversationId)!
|
|
307
|
+
if (isFirstRoot && conversation.title === DEFAULT_TITLE) {
|
|
308
|
+
conversations.set(conversationId, { ...conversation, title: generateTitle(trimmed) })
|
|
309
|
+
}
|
|
310
|
+
touch(conversationId)
|
|
311
|
+
|
|
312
|
+
if (!transport) {
|
|
313
|
+
status = 'idle'
|
|
314
|
+
emit()
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// history excludes the just-pushed user message
|
|
319
|
+
const history = historyFor(list, parentId).filter((m) => m.id !== userMsg.id)
|
|
320
|
+
const assistantMsg: ChatMessage = {
|
|
321
|
+
id: generateId('rfr-msg'),
|
|
322
|
+
conversationId,
|
|
323
|
+
role: 'assistant',
|
|
324
|
+
author: assistant,
|
|
325
|
+
content: '',
|
|
326
|
+
timestamp: new Date(),
|
|
327
|
+
status: 'streaming',
|
|
328
|
+
parentId,
|
|
329
|
+
}
|
|
330
|
+
list.push(assistantMsg)
|
|
331
|
+
await streamReply(conversationId, assistantMsg, userMsg, history)
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
async retryLast() {
|
|
335
|
+
if (!activeConversationId || !transport) return
|
|
336
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
337
|
+
const last = list[list.length - 1]
|
|
338
|
+
const parentId = last?.parentId
|
|
339
|
+
// Drop a trailing failed assistant message, if any.
|
|
340
|
+
if (last && last.role === 'assistant' && last.status === 'error') list.pop()
|
|
341
|
+
|
|
342
|
+
const scope = parentId ? selectThreadMessages(list, parentId) : selectRoots(list)
|
|
343
|
+
const lastUser = [...scope].reverse().find((m) => m.role === 'user')
|
|
344
|
+
if (!lastUser) return
|
|
345
|
+
|
|
346
|
+
const history = historyFor(list, parentId)
|
|
347
|
+
const assistantMsg: ChatMessage = {
|
|
348
|
+
id: generateId('rfr-msg'),
|
|
349
|
+
conversationId: activeConversationId,
|
|
350
|
+
role: 'assistant',
|
|
351
|
+
author: assistant,
|
|
352
|
+
content: '',
|
|
353
|
+
timestamp: new Date(),
|
|
354
|
+
status: 'streaming',
|
|
355
|
+
parentId,
|
|
356
|
+
}
|
|
357
|
+
list.push(assistantMsg)
|
|
358
|
+
await streamReply(activeConversationId, assistantMsg, lastUser, history)
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
stop() {
|
|
362
|
+
abortController?.abort()
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
openThread(rootId) {
|
|
366
|
+
openThreadRootId = rootId
|
|
367
|
+
replyTarget = rootId // reply to the root by default
|
|
368
|
+
emit()
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
replyTo(messageId) {
|
|
372
|
+
if (!activeConversationId) return
|
|
373
|
+
const list = messagesByConv.get(activeConversationId)!
|
|
374
|
+
// Open the originating thread, but target the specific message replied to.
|
|
375
|
+
openThreadRootId = rootIdOf(list, messageId)
|
|
376
|
+
replyTarget = messageId
|
|
377
|
+
emit()
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
closeThread() {
|
|
381
|
+
if (openThreadRootId === null && replyTarget === null) return
|
|
382
|
+
openThreadRootId = null
|
|
383
|
+
replyTarget = null
|
|
384
|
+
emit()
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
setThreadingMode(mode) {
|
|
388
|
+
if (threadingMode === mode) return
|
|
389
|
+
threadingMode = mode
|
|
390
|
+
emit()
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
setTransport(next) {
|
|
394
|
+
transport = next
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return api
|
|
399
|
+
}
|
|
@@ -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'
|