@sybilion/uilib 1.3.80 → 1.3.82
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/esm/components/ui/Chat/ChatChrome/ChatChrome.js +6 -4
- package/dist/esm/components/ui/Chat/ChatChrome/ChatChrome.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPresets/ChatPresets.js +2 -2
- package/dist/esm/components/ui/Chat/ChatPresets/ChatPresets.styl.js +2 -2
- package/dist/esm/components/ui/Chat/ChatSheet/useChatPanelChromeModel.js +2 -2
- package/dist/esm/contexts/chat-context.js +13 -20
- package/dist/esm/contexts/chatPersistence.js +67 -0
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/ChatChrome.types.d.ts +6 -1
- package/dist/esm/types/src/components/ui/Chat/ChatChrome/index.d.ts +1 -1
- package/dist/esm/types/src/components/ui/Chat/ChatPresets/ChatPresets.d.ts +3 -1
- package/dist/esm/types/src/components/ui/Chat/index.d.ts +1 -1
- package/dist/esm/types/src/contexts/chatPersistence.d.ts +5 -0
- package/dist/esm/types/src/contexts/chatPersistence.test.d.ts +1 -0
- package/dist/esm/types/tests/mocks/homecodeUiMock.d.ts +6 -0
- package/package.json +1 -1
- package/src/components/ui/Chat/ChatChrome/ChatChrome.styl +22 -4
- package/src/components/ui/Chat/ChatChrome/ChatChrome.styl.d.ts +4 -0
- package/src/components/ui/Chat/ChatChrome/ChatChrome.tsx +18 -9
- package/src/components/ui/Chat/ChatChrome/ChatChrome.types.ts +11 -1
- package/src/components/ui/Chat/ChatChrome/index.ts +1 -0
- package/src/components/ui/Chat/ChatPresets/ChatPresets.styl +2 -1
- package/src/components/ui/Chat/ChatPresets/ChatPresets.styl.d.ts +1 -0
- package/src/components/ui/Chat/ChatPresets/ChatPresets.tsx +10 -1
- package/src/components/ui/Chat/ChatSheet/useChatPanelChromeModel.tsx +6 -2
- package/src/components/ui/Chat/index.ts +1 -0
- package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts +3 -0
- package/src/contexts/chat-context.tsx +14 -20
- package/src/contexts/chatPersistence.test.ts +142 -0
- package/src/contexts/chatPersistence.ts +79 -0
- package/src/docs/pages/ChartAreaInteractivePage.tsx +7 -8
- package/src/docs/pages/ChatAttachmentsDropzonePage.tsx +1 -0
- package/src/docs/pages/ChatComposerPrefillPage.tsx +1 -0
- package/src/docs/pages/ChatPage.tsx +1 -1
- package/src/docs/pages/ChatSlashCommandsPage.tsx +1 -0
- package/src/docs/pages/ChatUserCsvAttachmentPage.tsx +1 -0
|
@@ -15,7 +15,7 @@ import { filterToTextAttachments, isAttachmentsDropzoneEnabled, buildAcceptAttr
|
|
|
15
15
|
import { extractChatAttachmentItems } from '../chatAttachmentExtract.js';
|
|
16
16
|
import S from './ChatChrome.styl.js';
|
|
17
17
|
|
|
18
|
-
function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, loadingLabel, scriptContinueLabel, onScriptContinue, renderMessageChart, renderSystemMessage, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, hideChatSelector = false, }) {
|
|
18
|
+
function ChatChrome({ variant = 'default', showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, loadingLabel, scriptContinueLabel, onScriptContinue, renderMessageChart, renderSystemMessage, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments = false, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, hideChatSelector = false, }) {
|
|
19
19
|
const filteredAllowedAttachments = useMemo(() => filterToTextAttachments(allowedAttachments), [allowedAttachments]);
|
|
20
20
|
const attachmentsDropzoneEnabled = isAttachmentsDropzoneEnabled(allowedAttachments, allowPdfAttachments);
|
|
21
21
|
const attachmentAccept = useMemo(() => buildAcceptAttr(filteredAllowedAttachments, allowPdfAttachments), [filteredAllowedAttachments, allowPdfAttachments]);
|
|
@@ -76,7 +76,8 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
76
76
|
showInlinePresets,
|
|
77
77
|
showSyntheticBranchButtons,
|
|
78
78
|
]);
|
|
79
|
-
|
|
79
|
+
const isClean = variant === 'clean';
|
|
80
|
+
return (jsxs("div", { className: cn(S.root, isClean && S.rootClean), children: [showResizeHandle && resizeHandle ? (jsx(PanelResizeHandle, { edge: "leading", isActive: resizeHandle.isActive, startWidthPx: resizeHandle.startWidthPx, getShellWidth: resizeHandle.getShellWidth, onDragWidth: resizeHandle.onDragWidth, onDragComplete: resizeHandle.onDragComplete, className: cn(SidebarStem.sidebarResizeHandle, S.chatResizeHandle) })) : null, jsx("div", { className: S.panelHeader, children: onClose ? (jsx(Button, { type: "button", variant: "ghost", icon: true, className: S.panelClose, "aria-label": "Close chat", onClick: onClose, children: jsx(X, { className: "size-4" }) })) : null }), jsxs("div", { className: S.content, children: [attachmentsDropzoneEnabled ? (jsx(DropZone, { accept: attachmentAccept, label: "Drop text files to attach", multiple: true, ghost: true, overlayScope: "container", disabled: promptDisabled, className: S.attachmentDropzone, onFiles: handleAttachmentFiles })) : null, jsxs(Chat, { className: isClean ? S.chatClean : undefined, isEmpty: isEmpty, scopeId: effectiveScopeId, onChatDeleted: onChatDeleted, onNewChat: onNewChat, hideChatSelector: hideChatSelector, children: [isEmpty ? (jsx("div", { className: S.emptyBody, children: jsx(Chat.EmptyState, { ...emptyState }) })) : (jsx("div", { className: S.scrollWrapper, children: jsxs(Scroll, { y: true, yScrollbarClassName: S.scrollbar, className: S.scroll, innerClassName: S.scrollInner, offset: { y: { before: 56, after: 24 } }, fadeSize: "m", autoHide: true, ref: scrollRef, children: [messages.map((msg, index, arr) => {
|
|
80
81
|
const isLast = index === arr.length - 1;
|
|
81
82
|
return (jsx(Chat.Message, { message: msg, role: msg.role, text: msg.text, inProgress: msg.inProgress, userTextFileAttachments: msg.userTextFileAttachments, onQuickReply: onQuickReply, suppressedQuickReplyKeys: suppressedQuickReplyKeys, quickReplyDisabled: isLoading, quickReplyHidden: Boolean(loadingLabel), isLastMessage: isLast, scriptContinue: isLast && scriptContinueLabel
|
|
82
83
|
? { label: scriptContinueLabel }
|
|
@@ -87,9 +88,10 @@ function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPr
|
|
|
87
88
|
const label = displayLabelForBranchKeyFromMessages(key, messages) ??
|
|
88
89
|
humanizeBranchKey(key);
|
|
89
90
|
return (jsx("span", { className: S.branchBtnWrap, children: jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: isLoading, onClick: () => onQuickReply(key, label), children: [jsx(PaperPlaneRightIcon, {}), label] }) }, key));
|
|
90
|
-
}) })) : null, showInlinePresets &&
|
|
91
|
+
}) })) : null, showInlinePresets &&
|
|
92
|
+
renderPresets('inline', { variant }), isLoading &&
|
|
91
93
|
!hasInProgressSystemMessage &&
|
|
92
|
-
(isLastMessageFromUser || loadingLabel) && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: loadingLabel ?? 'Thinking...' }))] }) })), isEmpty ? (jsx("div", { className: S.fixedPresets, children: renderPresets('fixed') })) : null,
|
|
94
|
+
(isLastMessageFromUser || loadingLabel) && (jsx(TextShimmer, { duration: 1, spread: 5, className: S.loader, children: loadingLabel ?? 'Thinking...' }))] }) })), isEmpty ? (jsx("div", { className: S.fixedPresets, children: renderPresets('fixed', { variant }) })) : null, isEmpty ? (jsx("div", { className: S.notice, children: "Forecast Assistant can make mistakes." })) : null, jsx("div", { className: cn(S.footer, isClean && S.footerClean, footerClassName), children: jsx(Chat.Prompt, { className: isClean ? S.promptClean : undefined, onSubmit: handlePromptSubmitWithAttachments, disabled: promptDisabled, attachments: pendingAttachments, onRemoveAttachment: handleRemoveAttachment, prefillMessage: promptPrefill ?? undefined, placeholder: promptPlaceholder, slashCommandItems: slashCommandItems, onSlashItemCommand: onSlashItemCommand, attachmentAccept: attachmentsDropzoneEnabled ? attachmentAccept : undefined, onAttachmentFiles: attachmentsDropzoneEnabled ? handleAttachmentFiles : undefined }) })] })] })] }));
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export { ChatChrome };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatChrome_root__oh4Ay{border-radius:var(--p-4);display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;position:relative}.ChatChrome_chatResizeHandle__epfiT{background-color:var(--page-color);border-radius:2.5px;height:calc(100vh + 200px);left:0;opacity:0;position:absolute;right:auto;top:-200px;touch-action:none;transition:opacity .15s ease-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5px;z-index:30}.ChatChrome_chatResizeHandle__epfiT:before{left:0;right:auto}.ChatChrome_panelHeader__Hkfit{align-items:center;display:flex;flex-shrink:0;justify-content:flex-end;min-height:70px;padding:var(--p-2) var(--p-3);position:absolute;width:100%;z-index:100}.ChatChrome_panelClose__DbKxz{flex-shrink:0}.ChatChrome_content__5qFEi{display:flex;flex:1;flex-direction:column;min-height:0;position:relative}.ChatChrome_attachmentDropzone__OC8UI{inset:0;position:absolute;z-index:200}.ChatChrome_scrollWrapper__m4HMu{flex:1;min-height:0;position:relative}.ChatChrome_scroll__oCxoJ{align-items:flex-end;height:100%;max-height:100%;max-width:100%;padding-bottom:var(--p-2);position:absolute;width:100%;z-index:3}.ChatChrome_scrollbar__Hu0aG{right:0!important}.ChatChrome_scrollInner__K9hIy{min-height:100%;padding-top:var(--p-10)}.ChatChrome_emptyBody__f2NE8{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.ChatChrome_emptyBody__f2NE8>*{flex:1;min-height:0;overflow:auto}.ChatChrome_footer__a5Bpp{backdrop-filter:blur(30px);background-color:var(--background-alpha-800);border-top:1px solid var(--border);box-shadow:0 8px 24px 0 var(--background);display:flex;flex-direction:column;flex-shrink:0;position:relative;width:100%;z-index:50}.ChatChrome_fixedPresets__bONhR{flex-shrink:0;position:relative;width:100%;z-index:10}.ChatChrome_notice__JACIw{color:var(--muted-foreground);font-size:var(--text-xs);
|
|
4
|
-
var S = {"root":"ChatChrome_root__oh4Ay","chatResizeHandle":"ChatChrome_chatResizeHandle__epfiT","panelHeader":"ChatChrome_panelHeader__Hkfit","panelClose":"ChatChrome_panelClose__DbKxz","content":"ChatChrome_content__5qFEi","attachmentDropzone":"ChatChrome_attachmentDropzone__OC8UI","scrollWrapper":"ChatChrome_scrollWrapper__m4HMu","scroll":"ChatChrome_scroll__oCxoJ","scrollbar":"ChatChrome_scrollbar__Hu0aG","scrollInner":"ChatChrome_scrollInner__K9hIy","emptyBody":"ChatChrome_emptyBody__f2NE8","footer":"ChatChrome_footer__a5Bpp","fixedPresets":"ChatChrome_fixedPresets__bONhR","notice":"ChatChrome_notice__JACIw","loader":"ChatChrome_loader__9-lnf","branchRow":"ChatChrome_branchRow__NMDNv","branchBtnWrap":"ChatChrome_branchBtnWrap__aOSVP"};
|
|
3
|
+
var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatChrome_root__oh4Ay{border-radius:var(--p-4);display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;position:relative}.ChatChrome_rootClean__WXM91{border-radius:0}.ChatChrome_chatResizeHandle__epfiT{background-color:var(--page-color);border-radius:2.5px;height:calc(100vh + 200px);left:0;opacity:0;position:absolute;right:auto;top:-200px;touch-action:none;transition:opacity .15s ease-out;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:5px;z-index:30}.ChatChrome_chatResizeHandle__epfiT:before{left:0;right:auto}.ChatChrome_panelHeader__Hkfit{align-items:center;display:flex;flex-shrink:0;justify-content:flex-end;min-height:70px;padding:var(--p-2) var(--p-3);position:absolute;width:100%;z-index:100}.ChatChrome_panelClose__DbKxz{flex-shrink:0}.ChatChrome_content__5qFEi{display:flex;flex:1;flex-direction:column;min-height:0;position:relative}.ChatChrome_chatClean__hM9yS{background-color:transparent}.ChatChrome_attachmentDropzone__OC8UI{inset:0;position:absolute;z-index:200}.ChatChrome_scrollWrapper__m4HMu{flex:1;min-height:0;position:relative}.ChatChrome_scroll__oCxoJ{align-items:flex-end;height:100%;max-height:100%;max-width:100%;padding-bottom:var(--p-2);position:absolute;width:100%;z-index:3}.ChatChrome_scrollbar__Hu0aG{right:0!important}.ChatChrome_scrollInner__K9hIy{min-height:100%;padding-top:var(--p-10)}.ChatChrome_emptyBody__f2NE8{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden}.ChatChrome_emptyBody__f2NE8>*{flex:1;min-height:0;overflow:auto}.ChatChrome_footer__a5Bpp{backdrop-filter:blur(30px);background-color:var(--background-alpha-800);border-top:1px solid var(--border);box-shadow:0 8px 24px 0 var(--background);display:flex;flex-direction:column;flex-shrink:0;position:relative;width:100%;z-index:50}.ChatChrome_footerClean__xRDdB{backdrop-filter:none;background-color:transparent;border-top:none;border:1px solid var(--border);border-radius:var(--p-4);box-shadow:none}.ChatChrome_promptClean__Hv43Z{background-color:var(--background);border-radius:inherit}.ChatChrome_fixedPresets__bONhR,.ChatChrome_notice__JACIw{flex-shrink:0;position:relative;width:100%;z-index:10}.ChatChrome_notice__JACIw{color:var(--muted-foreground);font-size:var(--text-xs);margin-bottom:var(--p-1);pointer-events:none;text-align:center}@media (max-width:768px){.ChatChrome_notice__JACIw{font-size:10px}}.ChatChrome_loader__9-lnf{color:var(--muted-foreground);margin:var(--p-2) var(--p-6) var(--p-10)}.ChatChrome_branchRow__NMDNv{display:flex;flex-wrap:wrap;gap:8px;margin-top:var(--p-6);padding:0 var(--p-6);width:100%}.ChatChrome_branchBtnWrap__aOSVP{display:inline-flex;vertical-align:middle}";
|
|
4
|
+
var S = {"root":"ChatChrome_root__oh4Ay","rootClean":"ChatChrome_rootClean__WXM91","chatResizeHandle":"ChatChrome_chatResizeHandle__epfiT","panelHeader":"ChatChrome_panelHeader__Hkfit","panelClose":"ChatChrome_panelClose__DbKxz","content":"ChatChrome_content__5qFEi","chatClean":"ChatChrome_chatClean__hM9yS","attachmentDropzone":"ChatChrome_attachmentDropzone__OC8UI","scrollWrapper":"ChatChrome_scrollWrapper__m4HMu","scroll":"ChatChrome_scroll__oCxoJ","scrollbar":"ChatChrome_scrollbar__Hu0aG","scrollInner":"ChatChrome_scrollInner__K9hIy","emptyBody":"ChatChrome_emptyBody__f2NE8","footer":"ChatChrome_footer__a5Bpp","footerClean":"ChatChrome_footerClean__xRDdB","promptClean":"ChatChrome_promptClean__Hv43Z","fixedPresets":"ChatChrome_fixedPresets__bONhR","notice":"ChatChrome_notice__JACIw","loader":"ChatChrome_loader__9-lnf","branchRow":"ChatChrome_branchRow__NMDNv","branchBtnWrap":"ChatChrome_branchBtnWrap__aOSVP"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { S as default };
|
|
@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
|
|
|
4
4
|
import { Button } from '../../Button/Button.js';
|
|
5
5
|
import S from './ChatPresets.styl.js';
|
|
6
6
|
|
|
7
|
-
function ChatPresets({ chatId, items, usedItemIds = [], layout = 'fixed', onSelect, onItemUsed, }) {
|
|
7
|
+
function ChatPresets({ chatId, items, usedItemIds = [], layout = 'fixed', variant = 'default', onSelect, onItemUsed, }) {
|
|
8
8
|
const [localUsedItems, setLocalUsedItems] = useState([]);
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
setLocalUsedItems([]);
|
|
@@ -13,7 +13,7 @@ function ChatPresets({ chatId, items, usedItemIds = [], layout = 'fixed', onSele
|
|
|
13
13
|
const availableItems = items.filter(item => !usedItems.includes(item.id));
|
|
14
14
|
if (availableItems.length === 0)
|
|
15
15
|
return null;
|
|
16
|
-
return (jsx("div", { className: layout === 'inline' ? S.inlineRoot : S.root, children: jsx("div", { className: cn(S.inner, layout === 'inline' && S.innerInline), children: availableItems.map(preset => (jsx(Button, { type: "button", variant: "outline", size: "sm", className: cn(S.item), onClick: () => {
|
|
16
|
+
return (jsx("div", { className: layout === 'inline' ? S.inlineRoot : S.root, children: jsx("div", { className: cn(S.inner, layout === 'inline' && S.innerInline, variant === 'clean' && S.innerClean), children: availableItems.map(preset => (jsx(Button, { type: "button", variant: "outline", size: "sm", className: cn(S.item), onClick: () => {
|
|
17
17
|
onSelect?.(preset);
|
|
18
18
|
onItemUsed?.(preset.id);
|
|
19
19
|
setLocalUsedItems(prev => [...prev, preset.id]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".ChatPresets_root__Cj42o{flex-shrink:0;position:relative;width:100%}.ChatPresets_inlineRoot__WXVnu{margin-top:var(--p-6);position:relative;width:100%}.ChatPresets_inner__h14-q{background-color:var(--background);display:flex;flex-wrap:wrap;gap:8px;min-width:0;padding:var(--p-2) var(--p-6) var(--p-3)}.ChatPresets_innerInline__iPM2b{background-color:transparent}.ChatPresets_item__LfX5b{flex:0 1 auto;font-size:var(--text-xs);height:auto;line-height:1.4;max-width:min(300px,100%);min-height:auto;min-width:0;overflow-wrap:anywhere;padding:var(--p-3);text-align:left;white-space:break-spaces!important}";
|
|
4
|
-
var S = {"root":"ChatPresets_root__Cj42o","inlineRoot":"ChatPresets_inlineRoot__WXVnu","inner":"ChatPresets_inner__h14-q","innerInline":"ChatPresets_innerInline__iPM2b","item":"ChatPresets_item__LfX5b"};
|
|
3
|
+
var css_248z = ".ChatPresets_root__Cj42o{flex-shrink:0;position:relative;width:100%}.ChatPresets_inlineRoot__WXVnu{margin-top:var(--p-6);position:relative;width:100%}.ChatPresets_inner__h14-q{background-color:var(--background);display:flex;flex-wrap:wrap;gap:8px;min-width:0;padding:var(--p-2) var(--p-6) var(--p-3)}.ChatPresets_innerClean__U-sL0,.ChatPresets_innerInline__iPM2b{background-color:transparent}.ChatPresets_item__LfX5b{flex:0 1 auto;font-size:var(--text-xs);height:auto;line-height:1.4;max-width:min(300px,100%);min-height:auto;min-width:0;overflow-wrap:anywhere;padding:var(--p-3);text-align:left;white-space:break-spaces!important}";
|
|
4
|
+
var S = {"root":"ChatPresets_root__Cj42o","inlineRoot":"ChatPresets_inlineRoot__WXVnu","inner":"ChatPresets_inner__h14-q","innerInline":"ChatPresets_innerInline__iPM2b","innerClean":"ChatPresets_innerClean__U-sL0","item":"ChatPresets_item__LfX5b"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
7
7
|
export { S as default };
|
|
@@ -813,10 +813,10 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
|
|
|
813
813
|
sidebarWidthPx,
|
|
814
814
|
chatWidthPx,
|
|
815
815
|
]);
|
|
816
|
-
const renderPresets = (layout = 'fixed') => {
|
|
816
|
+
const renderPresets = (layout = 'fixed', options) => {
|
|
817
817
|
if (!presetsWithFreeform?.length)
|
|
818
818
|
return null;
|
|
819
|
-
return (jsx(Chat.Presets, { chatId: currentChatId ?? '', items: presetsWithFreeform, layout: layout, usedItemIds: usedPresetIds, onSelect: preset => {
|
|
819
|
+
return (jsx(Chat.Presets, { chatId: currentChatId ?? '', items: presetsWithFreeform, layout: layout, variant: options?.variant, usedItemIds: usedPresetIds, onSelect: preset => {
|
|
820
820
|
void submitPreset(preset);
|
|
821
821
|
} }));
|
|
822
822
|
};
|
|
@@ -4,6 +4,7 @@ import { MessageRole } from '../components/ui/Chat/Chat.types.js';
|
|
|
4
4
|
import { normalizeUserTextFileAttachments } from '../components/ui/Chat/buildChatSendMessagePayload.js';
|
|
5
5
|
import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
|
|
6
6
|
import { LS } from '@homecode/ui';
|
|
7
|
+
import { persistChatsToLS, safeLsSet } from './chatPersistence.js';
|
|
7
8
|
|
|
8
9
|
const CHATS_PREFIX = 'chats-';
|
|
9
10
|
const CHAT_SCOPE_IDS_REGISTRY_KEY = 'chat-scope-ids';
|
|
@@ -77,7 +78,7 @@ function addScopeIdToRegistry(scopeId) {
|
|
|
77
78
|
const raw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
|
|
78
79
|
const registry = Array.isArray(raw) ? [...raw] : [];
|
|
79
80
|
if (!registry.includes(scopeId)) {
|
|
80
|
-
|
|
81
|
+
safeLsSet(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
/** Shallow-clone messages for seeding another session; drops in-progress rows. */
|
|
@@ -155,25 +156,23 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
155
156
|
setChats(prev => {
|
|
156
157
|
const currentChats = prev[scopeId] ?? [];
|
|
157
158
|
const updatedChats = [newChat, ...currentChats];
|
|
158
|
-
|
|
159
|
-
LS.set(chatsKey, updatedChats);
|
|
159
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
160
160
|
return {
|
|
161
161
|
...prev,
|
|
162
162
|
[scopeId]: updatedChats,
|
|
163
163
|
};
|
|
164
164
|
});
|
|
165
165
|
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
166
|
-
|
|
166
|
+
safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
|
|
167
167
|
return sessionId;
|
|
168
168
|
}, [userSwitchKey]);
|
|
169
169
|
const setCurrentChatId = useCallback((currScopeId, sessionId) => {
|
|
170
170
|
if (!sessionId)
|
|
171
171
|
return;
|
|
172
172
|
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
173
|
-
|
|
173
|
+
safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
|
|
174
174
|
}, []);
|
|
175
175
|
const deleteChat = useCallback((scopeId, sessionId) => {
|
|
176
|
-
const chatsKey = getChatsKey(scopeId);
|
|
177
176
|
const currentKey = getCurrentChatIdKey(scopeId);
|
|
178
177
|
setChats(prev => {
|
|
179
178
|
const scopeChats = prev[scopeId] ?? [];
|
|
@@ -182,7 +181,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
182
181
|
return prev;
|
|
183
182
|
}
|
|
184
183
|
const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
|
|
185
|
-
|
|
184
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
186
185
|
setCurrentChatIdState(prevCurr => {
|
|
187
186
|
if (prevCurr[scopeId] !== sessionId) {
|
|
188
187
|
return prevCurr;
|
|
@@ -191,7 +190,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
191
190
|
? scopeChats[deletedIndex - 1].session_id
|
|
192
191
|
: (updatedChats[0]?.session_id ?? null);
|
|
193
192
|
if (nextId) {
|
|
194
|
-
|
|
193
|
+
safeLsSet(currentKey, nextId);
|
|
195
194
|
}
|
|
196
195
|
else {
|
|
197
196
|
LS.remove(currentKey);
|
|
@@ -228,8 +227,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
228
227
|
}
|
|
229
228
|
return chat;
|
|
230
229
|
});
|
|
231
|
-
|
|
232
|
-
LS.set(chatsKey, updatedChats);
|
|
230
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
233
231
|
return { ...prev, [scopeId]: updatedChats };
|
|
234
232
|
});
|
|
235
233
|
return newMessage.id;
|
|
@@ -247,8 +245,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
247
245
|
messages: chat.messages.filter(m => m.id !== messageId),
|
|
248
246
|
};
|
|
249
247
|
});
|
|
250
|
-
|
|
251
|
-
LS.set(chatsKey, updatedChats);
|
|
248
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
252
249
|
return { ...prev, [scopeId]: updatedChats };
|
|
253
250
|
});
|
|
254
251
|
}, [userSwitchKey]);
|
|
@@ -286,8 +283,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
286
283
|
}),
|
|
287
284
|
};
|
|
288
285
|
});
|
|
289
|
-
|
|
290
|
-
LS.set(chatsKey, updatedChats);
|
|
286
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
291
287
|
return { ...prev, [scopeId]: updatedChats };
|
|
292
288
|
});
|
|
293
289
|
}, [userSwitchKey]);
|
|
@@ -306,8 +302,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
306
302
|
return chat;
|
|
307
303
|
return { ...chat, messages: cloned };
|
|
308
304
|
});
|
|
309
|
-
|
|
310
|
-
LS.set(chatsKey, updatedChats);
|
|
305
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
311
306
|
return { ...prev, [scopeId]: updatedChats };
|
|
312
307
|
});
|
|
313
308
|
}, [userSwitchKey]);
|
|
@@ -324,8 +319,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
324
319
|
meta: { ...chat.meta, ...patch },
|
|
325
320
|
};
|
|
326
321
|
});
|
|
327
|
-
|
|
328
|
-
LS.set(chatsKey, updatedChats);
|
|
322
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
329
323
|
return { ...prev, [scopeId]: updatedChats };
|
|
330
324
|
});
|
|
331
325
|
}, [userSwitchKey]);
|
|
@@ -362,8 +356,7 @@ function ChatProvider({ children, userSwitchKey, sendChatMessage: sendChatMessag
|
|
|
362
356
|
const updatedChats = scopeChats.map(chat => chat.session_id === pendingChatSessionId
|
|
363
357
|
? { ...chat, session_id: data.session_id }
|
|
364
358
|
: chat);
|
|
365
|
-
|
|
366
|
-
LS.set(chatsKey, updatedChats);
|
|
359
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
367
360
|
return { ...prev, [scopeId]: updatedChats };
|
|
368
361
|
});
|
|
369
362
|
setCurrentChatId(scopeId, data.session_id);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { stripJsonDashboardFences } from '../lib/dashboard-spec/stripJsonDashboardFences.js';
|
|
2
|
+
import { LS } from '@homecode/ui';
|
|
3
|
+
|
|
4
|
+
/** Ephemeral scopes (e.g. /reports/new draft) — keep only the latest session in LS. */
|
|
5
|
+
const EPHEMERAL_SCOPE_MARKERS = ['__reports_new_draft__'];
|
|
6
|
+
function isEphemeralChatScope(scopeId) {
|
|
7
|
+
return EPHEMERAL_SCOPE_MARKERS.some(marker => scopeId.endsWith(marker));
|
|
8
|
+
}
|
|
9
|
+
function isQuotaExceededError(error) {
|
|
10
|
+
if (!(error instanceof DOMException))
|
|
11
|
+
return false;
|
|
12
|
+
return (error.name === 'QuotaExceededError' ||
|
|
13
|
+
error.code === 22 ||
|
|
14
|
+
error.code === 1014);
|
|
15
|
+
}
|
|
16
|
+
function stripAttachmentForPersistence(attachment) {
|
|
17
|
+
return {
|
|
18
|
+
displayName: attachment.displayName,
|
|
19
|
+
filename: attachment.filename,
|
|
20
|
+
content: '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function stripMessageForPersistence(message) {
|
|
24
|
+
const text = stripJsonDashboardFences(message.text);
|
|
25
|
+
const next = { ...message, text };
|
|
26
|
+
if (message.inProgress) {
|
|
27
|
+
delete next.inProgress;
|
|
28
|
+
}
|
|
29
|
+
if (message.userTextFileAttachments?.length) {
|
|
30
|
+
next.userTextFileAttachments = message.userTextFileAttachments.map(stripAttachmentForPersistence);
|
|
31
|
+
}
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
function stripChatsForPersistence(chats) {
|
|
35
|
+
return chats.map(chat => ({
|
|
36
|
+
...chat,
|
|
37
|
+
messages: chat.messages
|
|
38
|
+
.filter(message => !message.inProgress)
|
|
39
|
+
.map(stripMessageForPersistence),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
function chatsForScopePersistence(scopeId, chats) {
|
|
43
|
+
const scopeChats = isEphemeralChatScope(scopeId) ? chats.slice(0, 1) : chats;
|
|
44
|
+
return stripChatsForPersistence(scopeChats);
|
|
45
|
+
}
|
|
46
|
+
function persistChatsToLS(scopeId, chats) {
|
|
47
|
+
const chatsKey = `chats-${scopeId}`;
|
|
48
|
+
const payload = chatsForScopePersistence(scopeId, chats);
|
|
49
|
+
try {
|
|
50
|
+
LS.set(chatsKey, payload);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!isQuotaExceededError(error))
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function safeLsSet(key, value) {
|
|
58
|
+
try {
|
|
59
|
+
LS.set(key, value);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (!isQuotaExceededError(error))
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { persistChatsToLS, safeLsSet, stripChatsForPersistence, stripMessageForPersistence };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { ChatChromeProps } from './ChatChrome.types';
|
|
2
|
-
export declare function ChatChrome({ showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, loadingLabel, scriptContinueLabel, onScriptContinue, renderMessageChart, renderSystemMessage, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, hideChatSelector, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function ChatChrome({ variant, showResizeHandle, resizeHandle, onClose, isEmpty, renderPresets, messages, onQuickReply, suppressedQuickReplyKeys, isLoading, loadingLabel, scriptContinueLabel, onScriptContinue, renderMessageChart, renderSystemMessage, showSyntheticBranchButtons, unusedBranchKeys, showInlinePresets, isLastMessageFromUser, scrollRef, effectiveScopeId, onPromptSubmit, onChatDeleted, onNewChat, promptPrefill, footerClassName, emptyState, allowedAttachments, allowPdfAttachments, onAttachmentsDropped, slashCommandItems, onSlashItemCommand, promptPlaceholder, hideChatSelector, }: ChatChromeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -12,12 +12,17 @@ export type ChatChromeResizeHandleConfig = {
|
|
|
12
12
|
onDragComplete: (finalRawPx: number) => void;
|
|
13
13
|
};
|
|
14
14
|
export type { ChatAttachmentDropItem };
|
|
15
|
+
export type ChatChromeVariant = 'default' | 'clean';
|
|
16
|
+
export type ChatPresetsRenderOptions = {
|
|
17
|
+
variant?: ChatChromeVariant;
|
|
18
|
+
};
|
|
15
19
|
export interface ChatChromeProps {
|
|
20
|
+
variant?: ChatChromeVariant;
|
|
16
21
|
showResizeHandle: boolean;
|
|
17
22
|
resizeHandle: ChatChromeResizeHandleConfig | undefined;
|
|
18
23
|
onClose: (() => void) | undefined;
|
|
19
24
|
isEmpty: boolean;
|
|
20
|
-
renderPresets: (layout: ChatPresetsLayout) => React.ReactNode;
|
|
25
|
+
renderPresets: (layout: ChatPresetsLayout, options?: ChatPresetsRenderOptions) => React.ReactNode;
|
|
21
26
|
/** Message list for the scroll area; use `[]` when empty. */
|
|
22
27
|
messages: Message[];
|
|
23
28
|
onQuickReply: (branchKey: string, displayLabel: string) => void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { ChatChrome } from './ChatChrome';
|
|
2
|
-
export type { ChatAttachmentDropItem, ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome.types';
|
|
2
|
+
export type { ChatAttachmentDropItem, ChatChromeProps, ChatChromeResizeHandleConfig, ChatChromeVariant, } from './ChatChrome.types';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChatChromeVariant } from '../ChatChrome/ChatChrome.types';
|
|
1
2
|
import type { ChatPreset } from '../Chat.types';
|
|
2
3
|
export type ChatPresetsLayout = 'fixed' | 'inline';
|
|
3
4
|
export interface ChatPresetsProps {
|
|
@@ -5,7 +6,8 @@ export interface ChatPresetsProps {
|
|
|
5
6
|
items: ChatPreset[];
|
|
6
7
|
usedItemIds?: string[];
|
|
7
8
|
layout?: ChatPresetsLayout;
|
|
9
|
+
variant?: ChatChromeVariant;
|
|
8
10
|
onSelect?: (preset: ChatPreset) => void;
|
|
9
11
|
onItemUsed?: (id: string) => void;
|
|
10
12
|
}
|
|
11
|
-
export declare function ChatPresets({ chatId, items, usedItemIds, layout, onSelect, onItemUsed, }: ChatPresetsProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function ChatPresets({ chatId, items, usedItemIds, layout, variant, onSelect, onItemUsed, }: ChatPresetsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Chat } from './Chat';
|
|
2
2
|
export { formatChatTranscript, usedPresetIdsFromMessages, } from './chat-preset-utils';
|
|
3
3
|
export { ChatChrome } from './ChatChrome';
|
|
4
|
-
export type { ChatChromeProps, ChatChromeResizeHandleConfig, } from './ChatChrome';
|
|
4
|
+
export type { ChatChromeProps, ChatChromeResizeHandleConfig, ChatChromeVariant, } from './ChatChrome';
|
|
5
5
|
export { TEXT_ATTACHMENT_ACCEPT_PARTS, filterToTextAttachments, } from './chatAttachmentAccept';
|
|
6
6
|
export { buildChatSendMessagePayload, displayTextFromSendPayload, normalizeUserTextFileAttachments, } from './buildChatSendMessagePayload';
|
|
7
7
|
export { sanitizeAttachmentFilename } from './sanitizeAttachmentFilename';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Chat, Message } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
export declare function stripMessageForPersistence(message: Message): Message;
|
|
3
|
+
export declare function stripChatsForPersistence(chats: Chat[]): Chat[];
|
|
4
|
+
export declare function persistChatsToLS(scopeId: string, chats: Chat[]): void;
|
|
5
|
+
export declare function safeLsSet(key: string, value: unknown): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
overflow hidden
|
|
10
10
|
border-radius var(--p-4)
|
|
11
11
|
|
|
12
|
+
.rootClean
|
|
13
|
+
border-radius 0
|
|
14
|
+
|
|
12
15
|
.chatResizeHandle
|
|
13
16
|
position: absolute;
|
|
14
17
|
// top 0 !important
|
|
@@ -57,6 +60,9 @@
|
|
|
57
60
|
flex-direction column
|
|
58
61
|
position relative
|
|
59
62
|
|
|
63
|
+
.chatClean
|
|
64
|
+
background-color transparent
|
|
65
|
+
|
|
60
66
|
.attachmentDropzone
|
|
61
67
|
position absolute
|
|
62
68
|
inset 0
|
|
@@ -111,6 +117,18 @@
|
|
|
111
117
|
border-top 1px solid var(--border)
|
|
112
118
|
box-shadow 0 8px 24px 0 var(--background)
|
|
113
119
|
|
|
120
|
+
.footerClean
|
|
121
|
+
backdrop-filter none
|
|
122
|
+
background-color transparent
|
|
123
|
+
border-top none
|
|
124
|
+
box-shadow none
|
|
125
|
+
border 1px solid var(--border)
|
|
126
|
+
border-radius var(--p-4)
|
|
127
|
+
|
|
128
|
+
.promptClean
|
|
129
|
+
background-color var(--background)
|
|
130
|
+
border-radius inherit
|
|
131
|
+
|
|
114
132
|
.fixedPresets
|
|
115
133
|
position relative
|
|
116
134
|
z-index 10
|
|
@@ -118,10 +136,10 @@
|
|
|
118
136
|
width 100%
|
|
119
137
|
|
|
120
138
|
.notice
|
|
121
|
-
position
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
position relative
|
|
140
|
+
z-index 10
|
|
141
|
+
flex-shrink 0
|
|
142
|
+
width 100%
|
|
125
143
|
margin-bottom var(--p-1)
|
|
126
144
|
|
|
127
145
|
font-size var(--text-xs)
|
|
@@ -4,16 +4,20 @@ interface CssExports {
|
|
|
4
4
|
'attachmentDropzone': string;
|
|
5
5
|
'branchBtnWrap': string;
|
|
6
6
|
'branchRow': string;
|
|
7
|
+
'chatClean': string;
|
|
7
8
|
'chatResizeHandle': string;
|
|
8
9
|
'content': string;
|
|
9
10
|
'emptyBody': string;
|
|
10
11
|
'fixedPresets': string;
|
|
11
12
|
'footer': string;
|
|
13
|
+
'footerClean': string;
|
|
12
14
|
'loader': string;
|
|
13
15
|
'notice': string;
|
|
14
16
|
'panelClose': string;
|
|
15
17
|
'panelHeader': string;
|
|
18
|
+
'promptClean': string;
|
|
16
19
|
'root': string;
|
|
20
|
+
'rootClean': string;
|
|
17
21
|
'scroll': string;
|
|
18
22
|
'scrollInner': string;
|
|
19
23
|
'scrollWrapper': string;
|
|
@@ -25,6 +25,7 @@ import S from './ChatChrome.styl';
|
|
|
25
25
|
import type { ChatChromeProps } from './ChatChrome.types';
|
|
26
26
|
|
|
27
27
|
export function ChatChrome({
|
|
28
|
+
variant = 'default',
|
|
28
29
|
showResizeHandle,
|
|
29
30
|
resizeHandle,
|
|
30
31
|
onClose,
|
|
@@ -150,8 +151,10 @@ export function ChatChrome({
|
|
|
150
151
|
showSyntheticBranchButtons,
|
|
151
152
|
]);
|
|
152
153
|
|
|
154
|
+
const isClean = variant === 'clean';
|
|
155
|
+
|
|
153
156
|
return (
|
|
154
|
-
<div className={S.root}>
|
|
157
|
+
<div className={cn(S.root, isClean && S.rootClean)}>
|
|
155
158
|
{showResizeHandle && resizeHandle ? (
|
|
156
159
|
<PanelResizeHandle
|
|
157
160
|
edge="leading"
|
|
@@ -191,6 +194,7 @@ export function ChatChrome({
|
|
|
191
194
|
/>
|
|
192
195
|
) : null}
|
|
193
196
|
<Chat
|
|
197
|
+
className={isClean ? S.chatClean : undefined}
|
|
194
198
|
isEmpty={isEmpty}
|
|
195
199
|
scopeId={effectiveScopeId}
|
|
196
200
|
onChatDeleted={onChatDeleted}
|
|
@@ -268,7 +272,8 @@ export function ChatChrome({
|
|
|
268
272
|
</div>
|
|
269
273
|
) : null}
|
|
270
274
|
|
|
271
|
-
{showInlinePresets &&
|
|
275
|
+
{showInlinePresets &&
|
|
276
|
+
renderPresets('inline', { variant })}
|
|
272
277
|
|
|
273
278
|
{isLoading &&
|
|
274
279
|
!hasInProgressSystemMessage &&
|
|
@@ -282,16 +287,20 @@ export function ChatChrome({
|
|
|
282
287
|
)}
|
|
283
288
|
|
|
284
289
|
{isEmpty ? (
|
|
285
|
-
<div className={S.fixedPresets}>
|
|
290
|
+
<div className={S.fixedPresets}>
|
|
291
|
+
{renderPresets('fixed', { variant })}
|
|
292
|
+
</div>
|
|
293
|
+
) : null}
|
|
294
|
+
|
|
295
|
+
{isEmpty ? (
|
|
296
|
+
<div className={S.notice}>
|
|
297
|
+
Forecast Assistant can make mistakes.
|
|
298
|
+
</div>
|
|
286
299
|
) : null}
|
|
287
300
|
|
|
288
|
-
<div className={cn(S.footer, footerClassName)}>
|
|
289
|
-
{isEmpty ? (
|
|
290
|
-
<div className={S.notice}>
|
|
291
|
-
Forecast Assistant can make mistakes.
|
|
292
|
-
</div>
|
|
293
|
-
) : null}
|
|
301
|
+
<div className={cn(S.footer, isClean && S.footerClean, footerClassName)}>
|
|
294
302
|
<Chat.Prompt
|
|
303
|
+
className={isClean ? S.promptClean : undefined}
|
|
295
304
|
onSubmit={handlePromptSubmitWithAttachments}
|
|
296
305
|
disabled={promptDisabled}
|
|
297
306
|
attachments={pendingAttachments}
|
|
@@ -22,12 +22,22 @@ export type ChatChromeResizeHandleConfig = {
|
|
|
22
22
|
|
|
23
23
|
export type { ChatAttachmentDropItem };
|
|
24
24
|
|
|
25
|
+
export type ChatChromeVariant = 'default' | 'clean';
|
|
26
|
+
|
|
27
|
+
export type ChatPresetsRenderOptions = {
|
|
28
|
+
variant?: ChatChromeVariant;
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
export interface ChatChromeProps {
|
|
32
|
+
variant?: ChatChromeVariant;
|
|
26
33
|
showResizeHandle: boolean;
|
|
27
34
|
resizeHandle: ChatChromeResizeHandleConfig | undefined;
|
|
28
35
|
onClose: (() => void) | undefined;
|
|
29
36
|
isEmpty: boolean;
|
|
30
|
-
renderPresets: (
|
|
37
|
+
renderPresets: (
|
|
38
|
+
layout: ChatPresetsLayout,
|
|
39
|
+
options?: ChatPresetsRenderOptions,
|
|
40
|
+
) => React.ReactNode;
|
|
31
41
|
/** Message list for the scroll area; use `[]` when empty. */
|
|
32
42
|
messages: Message[];
|
|
33
43
|
onQuickReply: (branchKey: string, displayLabel: string) => void;
|
|
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { Button } from '#uilib/components/ui/Button';
|
|
5
5
|
|
|
6
|
+
import type { ChatChromeVariant } from '../ChatChrome/ChatChrome.types';
|
|
6
7
|
import type { ChatPreset } from '../Chat.types';
|
|
7
8
|
import S from './ChatPresets.styl';
|
|
8
9
|
|
|
@@ -13,6 +14,7 @@ export interface ChatPresetsProps {
|
|
|
13
14
|
items: ChatPreset[];
|
|
14
15
|
usedItemIds?: string[];
|
|
15
16
|
layout?: ChatPresetsLayout;
|
|
17
|
+
variant?: ChatChromeVariant;
|
|
16
18
|
onSelect?: (preset: ChatPreset) => void;
|
|
17
19
|
onItemUsed?: (id: string) => void;
|
|
18
20
|
}
|
|
@@ -22,6 +24,7 @@ export function ChatPresets({
|
|
|
22
24
|
items,
|
|
23
25
|
usedItemIds = [],
|
|
24
26
|
layout = 'fixed',
|
|
27
|
+
variant = 'default',
|
|
25
28
|
onSelect,
|
|
26
29
|
onItemUsed,
|
|
27
30
|
}: ChatPresetsProps) {
|
|
@@ -38,7 +41,13 @@ export function ChatPresets({
|
|
|
38
41
|
|
|
39
42
|
return (
|
|
40
43
|
<div className={layout === 'inline' ? S.inlineRoot : S.root}>
|
|
41
|
-
<div
|
|
44
|
+
<div
|
|
45
|
+
className={cn(
|
|
46
|
+
S.inner,
|
|
47
|
+
layout === 'inline' && S.innerInline,
|
|
48
|
+
variant === 'clean' && S.innerClean,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
42
51
|
{availableItems.map(preset => (
|
|
43
52
|
<Button
|
|
44
53
|
key={preset.id}
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
} from '../../Sidebar/Sidebar';
|
|
52
52
|
import { Chat } from '../Chat';
|
|
53
53
|
import type { ChatChromeProps } from '../ChatChrome';
|
|
54
|
-
import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
|
|
54
|
+
import type { ChatAttachmentDropItem, ChatPresetsRenderOptions } from '../ChatChrome/ChatChrome.types';
|
|
55
55
|
import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
|
|
56
56
|
import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
|
|
57
57
|
import {
|
|
@@ -1106,7 +1106,10 @@ export function useChatPanelChromeModel({
|
|
|
1106
1106
|
chatWidthPx,
|
|
1107
1107
|
]);
|
|
1108
1108
|
|
|
1109
|
-
const renderPresets = (
|
|
1109
|
+
const renderPresets = (
|
|
1110
|
+
layout: ChatPresetsLayout = 'fixed',
|
|
1111
|
+
options?: ChatPresetsRenderOptions,
|
|
1112
|
+
) => {
|
|
1110
1113
|
if (!presetsWithFreeform?.length) return null;
|
|
1111
1114
|
|
|
1112
1115
|
return (
|
|
@@ -1114,6 +1117,7 @@ export function useChatPanelChromeModel({
|
|
|
1114
1117
|
chatId={currentChatId ?? ''}
|
|
1115
1118
|
items={presetsWithFreeform}
|
|
1116
1119
|
layout={layout}
|
|
1120
|
+
variant={options?.variant}
|
|
1117
1121
|
usedItemIds={usedPresetIds}
|
|
1118
1122
|
onSelect={preset => {
|
|
1119
1123
|
void submitPreset(preset);
|
package/src/components/widgets/DriversComparisonChart/driversComparisonChart.helpers.test.ts
CHANGED
|
@@ -262,6 +262,9 @@ describe('buildDriversComparisonChartData historical window floor', () => {
|
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
const datasetHistorical: ChartDataPoint[] = [
|
|
265
|
+
{ date: '2014-07-01', historical: 97 },
|
|
266
|
+
{ date: '2014-08-01', historical: 98 },
|
|
267
|
+
{ date: '2014-09-01', historical: 99 },
|
|
265
268
|
{ date: '2014-10-01', historical: 100 },
|
|
266
269
|
{ date: '2014-11-01', historical: 101 },
|
|
267
270
|
{ date: '2014-12-01', historical: 102 },
|
|
@@ -21,6 +21,8 @@ import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDas
|
|
|
21
21
|
import type { ChatResponse } from '#uilib/types/chat-api.types';
|
|
22
22
|
import { LS } from '@homecode/ui';
|
|
23
23
|
|
|
24
|
+
import { persistChatsToLS, safeLsSet } from './chatPersistence';
|
|
25
|
+
|
|
24
26
|
export type SendChatMessageFn = (
|
|
25
27
|
message: string,
|
|
26
28
|
targetChatId: string,
|
|
@@ -184,7 +186,7 @@ function addScopeIdToRegistry(scopeId: string) {
|
|
|
184
186
|
const raw = LS.get(CHAT_SCOPE_IDS_REGISTRY_KEY);
|
|
185
187
|
const registry = Array.isArray(raw) ? [...raw] : [];
|
|
186
188
|
if (!registry.includes(scopeId)) {
|
|
187
|
-
|
|
189
|
+
safeLsSet(CHAT_SCOPE_IDS_REGISTRY_KEY, [...registry, scopeId]);
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -300,8 +302,7 @@ export function ChatProvider({
|
|
|
300
302
|
const currentChats = prev[scopeId] ?? [];
|
|
301
303
|
const updatedChats = [newChat, ...currentChats];
|
|
302
304
|
|
|
303
|
-
|
|
304
|
-
LS.set(chatsKey, updatedChats);
|
|
305
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
305
306
|
|
|
306
307
|
return {
|
|
307
308
|
...prev,
|
|
@@ -309,7 +310,7 @@ export function ChatProvider({
|
|
|
309
310
|
};
|
|
310
311
|
});
|
|
311
312
|
setCurrentChatIdState(prev => ({ ...prev, [scopeId]: sessionId }));
|
|
312
|
-
|
|
313
|
+
safeLsSet(getCurrentChatIdKey(scopeId), sessionId);
|
|
313
314
|
return sessionId;
|
|
314
315
|
},
|
|
315
316
|
[userSwitchKey],
|
|
@@ -319,13 +320,12 @@ export function ChatProvider({
|
|
|
319
320
|
(currScopeId: string, sessionId: string) => {
|
|
320
321
|
if (!sessionId) return;
|
|
321
322
|
setCurrentChatIdState(prev => ({ ...prev, [currScopeId]: sessionId }));
|
|
322
|
-
|
|
323
|
+
safeLsSet(getCurrentChatIdKey(currScopeId), sessionId);
|
|
323
324
|
},
|
|
324
325
|
[],
|
|
325
326
|
);
|
|
326
327
|
|
|
327
328
|
const deleteChat = useCallback((scopeId: string, sessionId: string) => {
|
|
328
|
-
const chatsKey = getChatsKey(scopeId);
|
|
329
329
|
const currentKey = getCurrentChatIdKey(scopeId);
|
|
330
330
|
|
|
331
331
|
setChats(prev => {
|
|
@@ -337,7 +337,7 @@ export function ChatProvider({
|
|
|
337
337
|
return prev;
|
|
338
338
|
}
|
|
339
339
|
const updatedChats = scopeChats.filter(c => c.session_id !== sessionId);
|
|
340
|
-
|
|
340
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
341
341
|
|
|
342
342
|
setCurrentChatIdState(prevCurr => {
|
|
343
343
|
if (prevCurr[scopeId] !== sessionId) {
|
|
@@ -348,7 +348,7 @@ export function ChatProvider({
|
|
|
348
348
|
? scopeChats[deletedIndex - 1].session_id
|
|
349
349
|
: (updatedChats[0]?.session_id ?? null);
|
|
350
350
|
if (nextId) {
|
|
351
|
-
|
|
351
|
+
safeLsSet(currentKey, nextId);
|
|
352
352
|
} else {
|
|
353
353
|
LS.remove(currentKey);
|
|
354
354
|
}
|
|
@@ -395,8 +395,7 @@ export function ChatProvider({
|
|
|
395
395
|
return chat;
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
LS.set(chatsKey, updatedChats);
|
|
398
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
400
399
|
|
|
401
400
|
return { ...prev, [scopeId]: updatedChats };
|
|
402
401
|
});
|
|
@@ -417,8 +416,7 @@ export function ChatProvider({
|
|
|
417
416
|
messages: chat.messages.filter(m => m.id !== messageId),
|
|
418
417
|
};
|
|
419
418
|
});
|
|
420
|
-
|
|
421
|
-
LS.set(chatsKey, updatedChats);
|
|
419
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
422
420
|
return { ...prev, [scopeId]: updatedChats };
|
|
423
421
|
});
|
|
424
422
|
},
|
|
@@ -463,8 +461,7 @@ export function ChatProvider({
|
|
|
463
461
|
}),
|
|
464
462
|
};
|
|
465
463
|
});
|
|
466
|
-
|
|
467
|
-
LS.set(chatsKey, updatedChats);
|
|
464
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
468
465
|
return { ...prev, [scopeId]: updatedChats };
|
|
469
466
|
});
|
|
470
467
|
},
|
|
@@ -487,8 +484,7 @@ export function ChatProvider({
|
|
|
487
484
|
return { ...chat, messages: cloned };
|
|
488
485
|
});
|
|
489
486
|
|
|
490
|
-
|
|
491
|
-
LS.set(chatsKey, updatedChats);
|
|
487
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
492
488
|
|
|
493
489
|
return { ...prev, [scopeId]: updatedChats };
|
|
494
490
|
});
|
|
@@ -509,8 +505,7 @@ export function ChatProvider({
|
|
|
509
505
|
};
|
|
510
506
|
});
|
|
511
507
|
|
|
512
|
-
|
|
513
|
-
LS.set(chatsKey, updatedChats);
|
|
508
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
514
509
|
|
|
515
510
|
return { ...prev, [scopeId]: updatedChats };
|
|
516
511
|
});
|
|
@@ -577,8 +572,7 @@ export function ChatProvider({
|
|
|
577
572
|
: chat,
|
|
578
573
|
);
|
|
579
574
|
|
|
580
|
-
|
|
581
|
-
LS.set(chatsKey, updatedChats);
|
|
575
|
+
persistChatsToLS(scopeId, updatedChats);
|
|
582
576
|
|
|
583
577
|
return { ...prev, [scopeId]: updatedChats };
|
|
584
578
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { MessageRole } from '#uilib/components/ui/Chat/Chat.types';
|
|
2
|
+
import type { Chat } from '#uilib/components/ui/Chat/Chat.types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
persistChatsToLS,
|
|
6
|
+
stripChatsForPersistence,
|
|
7
|
+
stripMessageForPersistence,
|
|
8
|
+
} from './chatPersistence';
|
|
9
|
+
|
|
10
|
+
describe('stripMessageForPersistence', () => {
|
|
11
|
+
it('removes json-dashboard fences from message text', () => {
|
|
12
|
+
const message = stripMessageForPersistence({
|
|
13
|
+
id: '1',
|
|
14
|
+
role: MessageRole.ASSISTANT,
|
|
15
|
+
text: 'Summary\n```json-dashboard\n{"tiles":[]}\n```',
|
|
16
|
+
timestamp: 1,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(message.text).toBe('Summary');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('drops attachment content but keeps file metadata', () => {
|
|
23
|
+
const message = stripMessageForPersistence({
|
|
24
|
+
id: '2',
|
|
25
|
+
role: MessageRole.USER,
|
|
26
|
+
text: 'see file',
|
|
27
|
+
timestamp: 2,
|
|
28
|
+
userTextFileAttachments: [
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Report',
|
|
31
|
+
filename: 'report.pdf',
|
|
32
|
+
content: 'x'.repeat(5000),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(message.userTextFileAttachments).toEqual([
|
|
38
|
+
{ displayName: 'Report', filename: 'report.pdf', content: '' },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('stripChatsForPersistence', () => {
|
|
44
|
+
it('filters in-progress messages', () => {
|
|
45
|
+
const chats: Chat[] = [
|
|
46
|
+
{
|
|
47
|
+
session_id: 's1',
|
|
48
|
+
name: '',
|
|
49
|
+
messages: [
|
|
50
|
+
{
|
|
51
|
+
id: 'a',
|
|
52
|
+
role: MessageRole.SYSTEM,
|
|
53
|
+
text: 'Working...',
|
|
54
|
+
timestamp: 1,
|
|
55
|
+
inProgress: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'b',
|
|
59
|
+
role: MessageRole.USER,
|
|
60
|
+
text: 'hello',
|
|
61
|
+
timestamp: 2,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
expect(stripChatsForPersistence(chats)[0].messages).toHaveLength(1);
|
|
68
|
+
expect(stripChatsForPersistence(chats)[0].messages[0].id).toBe('b');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('persistChatsToLS', () => {
|
|
73
|
+
const quotaError = new DOMException('quota', 'QuotaExceededError');
|
|
74
|
+
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
window.localStorage.clear();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('swallows QuotaExceededError without throwing', () => {
|
|
80
|
+
const setItem = jest
|
|
81
|
+
.spyOn(Storage.prototype, 'setItem')
|
|
82
|
+
.mockImplementation(() => {
|
|
83
|
+
throw quotaError;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(() =>
|
|
87
|
+
persistChatsToLS('94-__reports_new_draft__', [
|
|
88
|
+
{
|
|
89
|
+
session_id: 's1',
|
|
90
|
+
name: '',
|
|
91
|
+
messages: [
|
|
92
|
+
{
|
|
93
|
+
id: '1',
|
|
94
|
+
role: MessageRole.USER,
|
|
95
|
+
text: 'hello',
|
|
96
|
+
timestamp: 1,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
]),
|
|
101
|
+
).not.toThrow();
|
|
102
|
+
|
|
103
|
+
setItem.mockRestore();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('persists only the latest session for ephemeral draft scopes', () => {
|
|
107
|
+
persistChatsToLS('94-__reports_new_draft__', [
|
|
108
|
+
{
|
|
109
|
+
session_id: 'current',
|
|
110
|
+
name: '',
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
id: '1',
|
|
114
|
+
role: MessageRole.USER,
|
|
115
|
+
text: 'current',
|
|
116
|
+
timestamp: 1,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
session_id: 'old',
|
|
122
|
+
name: '',
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
id: '2',
|
|
126
|
+
role: MessageRole.USER,
|
|
127
|
+
text: 'old',
|
|
128
|
+
timestamp: 2,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const stored = window.localStorage.getItem(
|
|
135
|
+
'chats-94-__reports_new_draft__',
|
|
136
|
+
);
|
|
137
|
+
expect(stored).not.toBeNull();
|
|
138
|
+
const parsed = JSON.parse(stored ?? '[]');
|
|
139
|
+
expect(parsed).toHaveLength(1);
|
|
140
|
+
expect(parsed[0].session_id).toBe('current');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Chat,
|
|
3
|
+
Message,
|
|
4
|
+
UserTextFileAttachment,
|
|
5
|
+
} from '#uilib/components/ui/Chat/Chat.types';
|
|
6
|
+
import { stripJsonDashboardFences } from '#uilib/lib/dashboard-spec/stripJsonDashboardFences';
|
|
7
|
+
import { LS } from '@homecode/ui';
|
|
8
|
+
|
|
9
|
+
/** Ephemeral scopes (e.g. /reports/new draft) — keep only the latest session in LS. */
|
|
10
|
+
const EPHEMERAL_SCOPE_MARKERS = ['__reports_new_draft__'] as const;
|
|
11
|
+
|
|
12
|
+
function isEphemeralChatScope(scopeId: string): boolean {
|
|
13
|
+
return EPHEMERAL_SCOPE_MARKERS.some(marker => scopeId.endsWith(marker));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isQuotaExceededError(error: unknown): boolean {
|
|
17
|
+
if (!(error instanceof DOMException)) return false;
|
|
18
|
+
return (
|
|
19
|
+
error.name === 'QuotaExceededError' ||
|
|
20
|
+
error.code === 22 ||
|
|
21
|
+
error.code === 1014
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripAttachmentForPersistence(
|
|
26
|
+
attachment: UserTextFileAttachment,
|
|
27
|
+
): UserTextFileAttachment {
|
|
28
|
+
return {
|
|
29
|
+
displayName: attachment.displayName,
|
|
30
|
+
filename: attachment.filename,
|
|
31
|
+
content: '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function stripMessageForPersistence(message: Message): Message {
|
|
36
|
+
const text = stripJsonDashboardFences(message.text);
|
|
37
|
+
const next: Message = { ...message, text };
|
|
38
|
+
if (message.inProgress) {
|
|
39
|
+
delete next.inProgress;
|
|
40
|
+
}
|
|
41
|
+
if (message.userTextFileAttachments?.length) {
|
|
42
|
+
next.userTextFileAttachments = message.userTextFileAttachments.map(
|
|
43
|
+
stripAttachmentForPersistence,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function stripChatsForPersistence(chats: Chat[]): Chat[] {
|
|
50
|
+
return chats.map(chat => ({
|
|
51
|
+
...chat,
|
|
52
|
+
messages: chat.messages
|
|
53
|
+
.filter(message => !message.inProgress)
|
|
54
|
+
.map(stripMessageForPersistence),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function chatsForScopePersistence(scopeId: string, chats: Chat[]): Chat[] {
|
|
59
|
+
const scopeChats = isEphemeralChatScope(scopeId) ? chats.slice(0, 1) : chats;
|
|
60
|
+
return stripChatsForPersistence(scopeChats);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function persistChatsToLS(scopeId: string, chats: Chat[]): void {
|
|
64
|
+
const chatsKey = `chats-${scopeId}`;
|
|
65
|
+
const payload = chatsForScopePersistence(scopeId, chats);
|
|
66
|
+
try {
|
|
67
|
+
LS.set(chatsKey, payload);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!isQuotaExceededError(error)) throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function safeLsSet(key: string, value: unknown): void {
|
|
74
|
+
try {
|
|
75
|
+
LS.set(key, value);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!isQuotaExceededError(error)) throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
import { ChartAreaInteractive } from '#uilib/components/ui/ChartAreaInteractive';
|
|
4
1
|
import type {
|
|
5
2
|
ChartDataPoint,
|
|
6
3
|
OverlayMode,
|
|
7
4
|
} from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
|
|
5
|
+
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
6
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
9
|
+
import { ChartAreaInteractive } from '#uilib/components/ui/ChartAreaInteractive';
|
|
10
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
11
|
+
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
8
12
|
import { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
9
13
|
import { Label } from '#uilib/components/ui/Label';
|
|
10
14
|
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
11
15
|
import { Switch } from '#uilib/components/ui/Switch';
|
|
12
|
-
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
13
16
|
import { useTheme } from '#uilib/contexts/theme-context';
|
|
14
|
-
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
15
|
-
|
|
16
|
-
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
17
|
-
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
18
17
|
|
|
19
18
|
const DEMO_FORECAST_ID = 1;
|
|
20
19
|
|