@iloveagents/foundry-web-ui 0.26.0 → 0.27.1
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/components/assistant-chat.d.ts +13 -2
- package/dist/components/assistant-chat.js +19 -14
- package/dist/components/chat-bubble.js +9 -6
- package/dist/components/chat-context.d.ts +2 -0
- package/dist/components/chat-context.js +70 -0
- package/dist/components/chat-slots.d.ts +10 -0
- package/dist/components/chat-slots.js +7 -0
- package/dist/components/composer-add-menu.js +3 -2
- package/dist/components/composer-input.d.ts +4 -0
- package/dist/components/composer-input.js +23 -0
- package/dist/components/context-badges.d.ts +4 -1
- package/dist/components/context-badges.js +7 -4
- package/dist/components/context-bar.d.ts +1 -0
- package/dist/components/context-bar.js +11 -3
- package/dist/components/data-table/data-table-frame.d.ts +1 -22
- package/dist/components/data-table/data-table-frame.js +21 -48
- package/dist/components/data-table/data-table-overflow.d.ts +5 -0
- package/dist/components/data-table/data-table-overflow.js +42 -0
- package/dist/components/data-table/data-table.js +88 -87
- package/dist/components/data-table/focus-pane-resize.d.ts +8 -0
- package/dist/components/data-table/focus-pane-resize.js +88 -0
- package/dist/components/focus-chat-pane.d.ts +8 -0
- package/dist/components/focus-chat-pane.js +9 -0
- package/dist/components/focus-dialog.d.ts +8 -0
- package/dist/components/focus-dialog.js +103 -0
- package/dist/components/focus-source-pane.d.ts +7 -0
- package/dist/components/focus-source-pane.js +15 -0
- package/dist/components/loading-indicator.js +1 -1
- package/dist/components/message-timestamp.js +3 -2
- package/dist/components/pin-state-icon.d.ts +5 -0
- package/dist/components/pin-state-icon.js +7 -0
- package/dist/components/pin-toggle.d.ts +7 -0
- package/dist/components/pin-toggle.js +10 -0
- package/dist/components/sidebar.d.ts +18 -1
- package/dist/components/sidebar.js +124 -74
- package/dist/components/tool-panel-layout.d.ts +3 -1
- package/dist/components/tool-panel-layout.js +5 -5
- package/dist/components/tool-panel.d.ts +8 -1
- package/dist/components/tool-panel.js +12 -8
- package/dist/components/user-menu.js +19 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/app-store.d.ts +0 -8
- package/dist/lib/app-store.js +14 -4
- package/dist/lib/chat-context-summary.d.ts +11 -0
- package/dist/lib/chat-context-summary.js +37 -0
- package/dist/lib/chat-lifecycle-store.d.ts +2 -0
- package/dist/lib/chat-lifecycle-store.js +6 -1
- package/dist/lib/client-tools.js +7 -0
- package/dist/lib/create-ui-id.d.ts +2 -0
- package/dist/lib/create-ui-id.js +9 -0
- package/dist/lib/focus-context.d.ts +2 -0
- package/dist/lib/focus-context.js +78 -0
- package/dist/lib/nav-config.d.ts +7 -0
- package/dist/lib/tool-panel-store.d.ts +7 -6
- package/dist/lib/tool-panel-store.js +29 -3
- package/dist/lib/use-new-conversation.js +1 -1
- package/dist/workbench/chat-header.d.ts +14 -0
- package/dist/workbench/chat-header.js +12 -0
- package/dist/workbench/chat-resize-handle.d.ts +4 -0
- package/dist/workbench/chat-resize-handle.js +40 -0
- package/dist/workbench/content-pane.d.ts +14 -0
- package/dist/workbench/content-pane.js +14 -0
- package/dist/workbench/conversation-history.d.ts +11 -0
- package/dist/workbench/conversation-history.js +83 -0
- package/dist/workbench/navigation.d.ts +6 -0
- package/dist/workbench/navigation.js +6 -0
- package/dist/workbench/selection-bridge.d.ts +5 -0
- package/dist/workbench/selection-bridge.js +84 -0
- package/dist/workbench/use-chat-width.d.ts +14 -0
- package/dist/workbench/use-chat-width.js +56 -0
- package/dist/workbench/use-workbench-state.d.ts +50 -0
- package/dist/workbench/use-workbench-state.js +145 -0
- package/dist/workbench/welcome-intro.d.ts +6 -0
- package/dist/workbench/welcome-intro.js +10 -0
- package/dist/workbench/workbench-styles.d.ts +1 -0
- package/dist/workbench/workbench-styles.js +290 -0
- package/dist/workbench/workbench.d.ts +5 -0
- package/dist/workbench/workbench.js +32 -0
- package/dist/workbench.d.ts +2 -0
- package/dist/workbench.js +2 -0
- package/package.json +8 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, } from "../index.js";
|
|
3
|
+
import { Button, Input } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { Fragment, useEffect, useState } from "react";
|
|
5
|
+
import { Link } from "react-router";
|
|
6
|
+
import { ArrowLeft, MessageSquare, MoreHorizontal, Pin, Loader2, ChevronDown, Check, } from "lucide-react";
|
|
7
|
+
/** History is a view of existing navigation targets, never a second chat runtime. */
|
|
8
|
+
export function ConversationHistory({ groups, workspaceId, onClose, showBack = true, welcome = false, compact = false, onViewAll, }) {
|
|
9
|
+
const [loadingTo, setLoadingTo] = useState(null);
|
|
10
|
+
const [loadError, setLoadError] = useState(null);
|
|
11
|
+
const [all, setAll] = useState(!workspaceId);
|
|
12
|
+
useEffect(() => setAll(!workspaceId), [workspaceId]);
|
|
13
|
+
const [search, setSearch] = useState("");
|
|
14
|
+
const scoped = groups.find((g) => g.meta?.type === "recent-chats" && g.meta?.workspaceId === workspaceId);
|
|
15
|
+
const allItems = scoped?.meta?.allConversations ??
|
|
16
|
+
groups
|
|
17
|
+
.filter((g) => g.meta?.type === "recent-chats" || g.meta?.type === "pinned-chats")
|
|
18
|
+
.flatMap((g) => g.items);
|
|
19
|
+
const scopedItems = all || !workspaceId
|
|
20
|
+
? allItems
|
|
21
|
+
: (scoped?.meta?.workspaceConversations ?? scoped?.items ?? []);
|
|
22
|
+
const items = [...new Map(scopedItems.map((item) => [item.to, item])).values()]
|
|
23
|
+
.filter((item) => item.meta?.type !== "show-all-chats")
|
|
24
|
+
.filter((item) => item.label.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
|
|
25
|
+
.sort((a, b) => Number(Boolean(b.meta?.pinned)) - Number(Boolean(a.meta?.pinned)));
|
|
26
|
+
const visibleItems = compact ? items.slice(0, 3) : items;
|
|
27
|
+
const pinnedItems = visibleItems.filter((item) => item.meta?.pinned);
|
|
28
|
+
const sections = pinnedItems.length
|
|
29
|
+
? [
|
|
30
|
+
{ label: "Pinned", items: pinnedItems },
|
|
31
|
+
{ label: "Recent chats", items: visibleItems.filter((item) => !item.meta?.pinned) },
|
|
32
|
+
]
|
|
33
|
+
: [{ label: "", items: visibleItems }];
|
|
34
|
+
return (_jsxs("section", { "data-conversation-history": true, "aria-label": "Chat history", className: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col px-4 pb-4", children: [welcome && (_jsxs("div", { className: "pb-6 pt-8", children: [_jsx("h1", { className: "text-xl font-semibold", children: "How can I help?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Start something new or continue a recent chat." })] })), showBack && (_jsxs("button", { onClick: onClose, className: "mb-4 flex items-center gap-2 py-2 text-xs text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { size: 14 }), " Back to chat"] })), _jsxs("div", { className: "mb-3 mt-2 flex items-center justify-between", children: [_jsx("h2", { className: compact ? "text-xs font-medium text-muted-foreground" : "text-base font-semibold", children: "Conversation history" }), compact && (_jsx("button", { onClick: onViewAll, className: "text-xs text-muted-foreground hover:text-foreground", children: "View all" }))] }), Boolean(workspaceId) && (_jsx("div", { className: "mb-3", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", "aria-label": "History filter", className: "w-full justify-between font-normal shadow-none", children: [_jsx("span", { className: "truncate", children: all ? "All conversations" : "Last used in this workspace" }), _jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "w-[var(--radix-dropdown-menu-trigger-width)]", children: [
|
|
35
|
+
{ value: false, label: "Last used in this workspace" },
|
|
36
|
+
{ value: true, label: "All conversations" },
|
|
37
|
+
].map((option) => (_jsxs(DropdownMenuItem, { role: "menuitemradio", "aria-checked": all === option.value, onSelect: () => setAll(option.value), children: [_jsx("span", { className: "flex-1", children: option.label }), all === option.value && _jsx(Check, { className: "size-4", "aria-hidden": "true" })] }, option.label))) })] }) })), !compact && (_jsx(Input, { "aria-label": "Search chats", placeholder: "Search chats", value: search, onChange: (event) => setSearch(event.target.value), className: "mb-3" })), loadError && (_jsx("p", { role: "alert", className: "mb-2 text-xs text-destructive", children: loadError })), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", "aria-busy": Boolean(loadingTo), children: [sections
|
|
38
|
+
.filter((section) => section.items.length)
|
|
39
|
+
.map((section) => (_jsxs("section", { "aria-label": section.label || "Recent chats", className: "mb-4 last:mb-0", children: [section.label && (_jsx("h3", { "data-conversation-section-label": true, className: "px-2 py-2 text-xs font-medium text-muted-foreground", children: section.label })), section.items.map((item) => item.disabled ? (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: item.label }, item.to)) : (_jsxs("div", { className: "group flex items-center rounded-lg hover:bg-muted", children: [_jsxs(Link, { to: item.to, onClick: (event) => {
|
|
40
|
+
if (loadingTo) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
|
|
45
|
+
return;
|
|
46
|
+
{
|
|
47
|
+
const resume = new CustomEvent("preview:resume-chat", {
|
|
48
|
+
cancelable: true,
|
|
49
|
+
detail: {
|
|
50
|
+
to: item.to,
|
|
51
|
+
complete: (error) => {
|
|
52
|
+
setLoadingTo(null);
|
|
53
|
+
if (error)
|
|
54
|
+
setLoadError(error);
|
|
55
|
+
else
|
|
56
|
+
onClose();
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
setLoadingTo(item.to);
|
|
61
|
+
setLoadError(null);
|
|
62
|
+
if (!window.dispatchEvent(resume)) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
setLoadingTo(null);
|
|
68
|
+
onClose();
|
|
69
|
+
}, "data-conversation-row": true, className: "flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 py-1.5 text-sm hover:bg-muted focus-visible:outline-2 focus-visible:outline-primary", children: [loadingTo === item.to ? (_jsx(Loader2, { className: "size-4 shrink-0 animate-spin text-muted-foreground", "aria-label": "Opening chat" })) : item.meta?.pinned ? (_jsx(Pin, { className: "size-4 shrink-0 text-muted-foreground", "aria-label": "Pinned chat" })) : (_jsx(MessageSquare, { className: "size-4 shrink-0 text-muted-foreground" })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate", children: item.label }), " ", _jsx("span", { className: "block truncate text-xs text-muted-foreground", children: !all &&
|
|
70
|
+
typeof item.meta?.previewEntityName === "string" &&
|
|
71
|
+
item.meta.previewEntityName
|
|
72
|
+
? `Last at ${item.meta.previewEntityName}`
|
|
73
|
+
: !all
|
|
74
|
+
? "Last in this workspace"
|
|
75
|
+
: typeof item.meta?.previewSpaceName === "string" &&
|
|
76
|
+
item.meta.previewSpaceName
|
|
77
|
+
? `Last in ${item.meta.previewSpaceName}`
|
|
78
|
+
: item.meta?.previewContextUnavailable
|
|
79
|
+
? "Location unavailable"
|
|
80
|
+
: "Last outside a workspace" })] })] }), Boolean(item.actions?.length) && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": `Actions for ${item.label}`, className: "mr-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 data-[state=open]:opacity-100 [@media(hover:none)]:opacity-100 hover:bg-background", children: _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", children: item.actions?.map((action) => (_jsxs(Fragment, { children: [action.separator && _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: () => {
|
|
81
|
+
void action.handler();
|
|
82
|
+
}, className: action.destructive ? "text-destructive" : undefined, children: [_jsx(action.icon, { className: "mr-2 size-4" }), action.label] })] }, action.label))) })] }))] }, item.to)))] }, section.label))), items.length === 0 && (_jsx("p", { className: "py-3 text-sm text-muted-foreground", children: search ? "No matching chats" : "No chats yet" }))] })] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Sidebar, UserMenu, ThemeToggle } from "../index.js";
|
|
3
|
+
import { SelectionBridge } from "./selection-bridge.js";
|
|
4
|
+
export function WorkbenchNavigation({ groups, workspace, onNewConversation, }) {
|
|
5
|
+
return (_jsx(SelectionBridge, { children: _jsx(Sidebar, { groups: groups, collapsedGroupsOnly: true, hideNewConversation: true, newConversationLabel: "New chat", hideFooterActions: workspace, onNewConversation: onNewConversation, footer: _jsx("footer", { className: "shrink-0 border-t border-sidebar-border bg-sidebar px-2 py-2", "aria-label": "Account and appearance", children: _jsxs("div", { className: "flex min-w-0 items-center justify-between gap-1", children: [_jsx(UserMenu, {}), _jsx(ThemeToggle, {})] }) }), collapsedFooter: _jsxs("footer", { className: "preview-account-compact flex shrink-0 flex-col items-center border-t border-sidebar-border py-2", "aria-label": "Account and appearance", children: [_jsx(ThemeToggle, {}), _jsx(UserMenu, {})] }) }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
/** One continuous contour connects the selected row to the chat boundary. */
|
|
4
|
+
export function SelectionBridge({ children }) {
|
|
5
|
+
const root = useRef(null);
|
|
6
|
+
const [anchor, setAnchor] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const node = root.current;
|
|
9
|
+
if (!node)
|
|
10
|
+
return;
|
|
11
|
+
let frame = 0;
|
|
12
|
+
const measure = () => {
|
|
13
|
+
cancelAnimationFrame(frame);
|
|
14
|
+
frame = requestAnimationFrame(() => {
|
|
15
|
+
const bounds = node.getBoundingClientRect();
|
|
16
|
+
const row = node.querySelector('[data-sidebar-surface="desktop"] [data-active-nav-row="true"]');
|
|
17
|
+
const rect = row?.getBoundingClientRect();
|
|
18
|
+
const scroll = row?.closest("nav")?.getBoundingClientRect();
|
|
19
|
+
const visible = rect &&
|
|
20
|
+
bounds.width > 80 &&
|
|
21
|
+
rect.height > 0 &&
|
|
22
|
+
(!scroll || (rect.top >= scroll.top && rect.bottom <= scroll.bottom));
|
|
23
|
+
setAnchor(visible
|
|
24
|
+
? {
|
|
25
|
+
top: rect.top - bounds.top,
|
|
26
|
+
height: rect.height,
|
|
27
|
+
left: rect.left - bounds.left + 0.5,
|
|
28
|
+
rowRight: rect.right - bounds.left - 1,
|
|
29
|
+
width: bounds.width,
|
|
30
|
+
navHeight: bounds.height,
|
|
31
|
+
surfaceWidth: node.nextElementSibling?.getBoundingClientRect().width ?? 0,
|
|
32
|
+
}
|
|
33
|
+
: null);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
|
|
37
|
+
const mutation = typeof MutationObserver === "undefined" ? null : new MutationObserver(measure);
|
|
38
|
+
resize?.observe(node);
|
|
39
|
+
if (node.nextElementSibling)
|
|
40
|
+
resize?.observe(node.nextElementSibling);
|
|
41
|
+
if (node.parentElement)
|
|
42
|
+
resize?.observe(node.parentElement);
|
|
43
|
+
mutation?.observe(node, {
|
|
44
|
+
subtree: true,
|
|
45
|
+
childList: true,
|
|
46
|
+
attributes: true,
|
|
47
|
+
attributeFilter: ["class", "style", "data-active-nav-row"],
|
|
48
|
+
});
|
|
49
|
+
node.addEventListener("scroll", measure, true);
|
|
50
|
+
measure();
|
|
51
|
+
return () => {
|
|
52
|
+
cancelAnimationFrame(frame);
|
|
53
|
+
resize?.disconnect();
|
|
54
|
+
mutation?.disconnect();
|
|
55
|
+
node.removeEventListener("scroll", measure, true);
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
return (_jsxs("div", { ref: root, "data-connected-navigation": anchor ? "true" : undefined, className: "relative flex shrink-0", children: [_jsx("style", { children: `
|
|
59
|
+
[data-connected-navigation="true"] + [data-workbench-surface]::after { display: none; }
|
|
60
|
+
.workbench-preview [data-sidebar-surface="desktop"] nav { padding-right: 16px; }
|
|
61
|
+
.workbench-preview [data-sidebar-surface="desktop"] { border-right-color: transparent; }
|
|
62
|
+
[data-connected-navigation="true"] [data-sidebar-surface="desktop"] [data-active-nav-row="true"] {
|
|
63
|
+
background: var(--background);
|
|
64
|
+
border-color: transparent; border-top-right-radius: 0; border-bottom-right-radius: 0;
|
|
65
|
+
}
|
|
66
|
+
` }), children, anchor && (_jsxs("svg", { "aria-hidden": "true", className: "pointer-events-none absolute z-20", style: { inset: 0, width: anchor.width + anchor.surfaceWidth, height: anchor.navHeight }, viewBox: `0 0 ${anchor.width + anchor.surfaceWidth} ${anchor.navHeight}`, children: [_jsx("path", { fill: "var(--background)", d: `M ${anchor.width},${anchor.top - 7} Q ${anchor.width},${anchor.top} ${anchor.width - 8},${anchor.top}
|
|
67
|
+
H ${anchor.rowRight} V ${anchor.top + anchor.height} H ${anchor.width - 8}
|
|
68
|
+
Q ${anchor.width},${anchor.top + anchor.height} ${anchor.width},${anchor.top + anchor.height + 7} Z` }), _jsx("path", { fill: "none", stroke: "var(--sidebar-ring)", strokeOpacity: 0.28, strokeWidth: 1, d: `M ${anchor.width + 12},0.5
|
|
69
|
+
H ${anchor.width + anchor.surfaceWidth - 12}
|
|
70
|
+
Q ${anchor.width + anchor.surfaceWidth - 0.5},0.5 ${anchor.width + anchor.surfaceWidth - 0.5},12
|
|
71
|
+
V ${anchor.navHeight - 12}
|
|
72
|
+
Q ${anchor.width + anchor.surfaceWidth - 0.5},${anchor.navHeight - 0.5} ${anchor.width + anchor.surfaceWidth - 12},${anchor.navHeight - 0.5}
|
|
73
|
+
H ${anchor.width + 12}
|
|
74
|
+
Q ${anchor.width - 0.5},${anchor.navHeight - 0.5} ${anchor.width - 0.5},${anchor.navHeight - 12}
|
|
75
|
+
V ${anchor.top + anchor.height + 7}
|
|
76
|
+
Q ${anchor.width - 0.5},${anchor.top + anchor.height - 0.5} ${anchor.width - 8},${anchor.top + anchor.height - 0.5}
|
|
77
|
+
H ${anchor.left + 12}
|
|
78
|
+
Q ${anchor.left},${anchor.top + anchor.height - 0.5} ${anchor.left},${anchor.top + anchor.height - 12}
|
|
79
|
+
V ${anchor.top + 12}
|
|
80
|
+
Q ${anchor.left},${anchor.top + 0.5} ${anchor.left + 12},${anchor.top + 0.5}
|
|
81
|
+
H ${anchor.width - 8}
|
|
82
|
+
Q ${anchor.width - 0.5},${anchor.top + 0.5} ${anchor.width - 0.5},${anchor.top - 7}
|
|
83
|
+
V 12 Q ${anchor.width - 0.5},0.5 ${anchor.width + 12},0.5 Z` })] }))] }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function chatWidthLimits(available: number): {
|
|
2
|
+
min: number;
|
|
3
|
+
max: number;
|
|
4
|
+
defaultWidth: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function useChatWidth(): {
|
|
7
|
+
min: number;
|
|
8
|
+
max: number;
|
|
9
|
+
defaultWidth: number;
|
|
10
|
+
containerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
11
|
+
width: number;
|
|
12
|
+
setWidth: (value: number | null) => void;
|
|
13
|
+
compact: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
const WIDTH_KEY = "foundry.workbench-preview.chat-width";
|
|
3
|
+
const MIN_WIDTH = 360;
|
|
4
|
+
const CONTENT_MIN = 440;
|
|
5
|
+
export function chatWidthLimits(available) {
|
|
6
|
+
const max = Math.max(MIN_WIDTH, Math.min(640, available - CONTENT_MIN));
|
|
7
|
+
return {
|
|
8
|
+
min: MIN_WIDTH,
|
|
9
|
+
max,
|
|
10
|
+
defaultWidth: Math.min(max, Math.max(MIN_WIDTH, Math.min(420, Math.round(available * 0.36)))),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function useChatWidth() {
|
|
14
|
+
const containerRef = useRef(null);
|
|
15
|
+
const [available, setAvailable] = useState(1000);
|
|
16
|
+
const [preferred, setPreferred] = useState(() => {
|
|
17
|
+
try {
|
|
18
|
+
const stored = Number(localStorage.getItem(WIDTH_KEY));
|
|
19
|
+
return Number.isFinite(stored) && stored >= MIN_WIDTH ? Math.min(stored, 640) : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const node = containerRef.current;
|
|
27
|
+
if (!node)
|
|
28
|
+
return;
|
|
29
|
+
const measure = () => {
|
|
30
|
+
if (node.clientWidth > 0)
|
|
31
|
+
setAvailable(node.clientWidth);
|
|
32
|
+
};
|
|
33
|
+
measure();
|
|
34
|
+
if (typeof ResizeObserver === "undefined")
|
|
35
|
+
return;
|
|
36
|
+
const observer = new ResizeObserver(measure);
|
|
37
|
+
observer.observe(node);
|
|
38
|
+
return () => observer.disconnect();
|
|
39
|
+
}, []);
|
|
40
|
+
const limits = chatWidthLimits(available);
|
|
41
|
+
const width = Math.min(limits.max, Math.max(limits.min, preferred ?? limits.defaultWidth));
|
|
42
|
+
const setWidth = (value) => {
|
|
43
|
+
const next = value === null ? null : Math.max(limits.min, Math.min(limits.max, value));
|
|
44
|
+
setPreferred(next);
|
|
45
|
+
try {
|
|
46
|
+
if (next === null)
|
|
47
|
+
localStorage.removeItem(WIDTH_KEY);
|
|
48
|
+
else
|
|
49
|
+
localStorage.setItem(WIDTH_KEY, String(next));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* Storage may be disabled. */
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return { containerRef, width, setWidth, compact: available < MIN_WIDTH + CONTENT_MIN, ...limits };
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare function useWorkbenchState(chatRoute: boolean): {
|
|
2
|
+
panelOpen: boolean;
|
|
3
|
+
chatOnly: boolean;
|
|
4
|
+
pageLabel: string;
|
|
5
|
+
chatWidth: {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
defaultWidth: number;
|
|
9
|
+
containerRef: import("react").RefObject<HTMLDivElement | null>;
|
|
10
|
+
width: number;
|
|
11
|
+
setWidth: (value: number | null) => void;
|
|
12
|
+
compact: boolean;
|
|
13
|
+
};
|
|
14
|
+
collapsed: boolean;
|
|
15
|
+
setCollapsed: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
16
|
+
compactPane: "content" | "chat";
|
|
17
|
+
setCompactPane: import("react").Dispatch<import("react").SetStateAction<"content" | "chat">>;
|
|
18
|
+
contentCollapsed: boolean;
|
|
19
|
+
setContentCollapsed: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
20
|
+
contentHidden: boolean;
|
|
21
|
+
historyOpen: boolean;
|
|
22
|
+
emptyChat: boolean;
|
|
23
|
+
showHistory: boolean;
|
|
24
|
+
spaciousChat: boolean;
|
|
25
|
+
returnToChat: () => void;
|
|
26
|
+
openHistory: () => void;
|
|
27
|
+
historyShortcut: import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
toggleNavigation: () => void;
|
|
29
|
+
navGroups: import("../index.js").NavGroup[];
|
|
30
|
+
workspaceGroups: import("../index.js").NavGroup[] | undefined;
|
|
31
|
+
staticNavigationGroups: {
|
|
32
|
+
collapsible: boolean;
|
|
33
|
+
label: string;
|
|
34
|
+
tree?: boolean;
|
|
35
|
+
icon?: import("lucide-react").LucideIcon;
|
|
36
|
+
items: import("../index.js").NavItem[];
|
|
37
|
+
priority?: number;
|
|
38
|
+
defaultCollapsed?: boolean;
|
|
39
|
+
to?: string;
|
|
40
|
+
hideLabel?: boolean;
|
|
41
|
+
focusOn?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
meta?: Record<string, unknown>;
|
|
44
|
+
contextInstructions?: string;
|
|
45
|
+
createActions?: import("../index.js").NavGroupCreateAction[];
|
|
46
|
+
collapsedRail?: (ctx: import("../index.js").CollapsedRailContext) => import("../index.js").CollapsedRailItem[];
|
|
47
|
+
}[];
|
|
48
|
+
closePanel: () => void;
|
|
49
|
+
startChat: () => void;
|
|
50
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { useSidebarStore, useChatBubbleStore, useToolPanelStore, useNewConversation, useNavStore, useAppStore, } from "../index.js";
|
|
7
|
+
import { History, Settings, Layers } from "lucide-react";
|
|
8
|
+
import { useChatWidth } from "./use-chat-width.js";
|
|
9
|
+
function useWorkbenchNavigation() {
|
|
10
|
+
const navGroups = useNavStore((s) => s.config);
|
|
11
|
+
const workspaceGroups = useMemo(() => {
|
|
12
|
+
const workspace = navGroups.find((group) => group.meta?.type === "spaces-workspaces" && group.meta?.exclusiveRootId);
|
|
13
|
+
if (!workspace)
|
|
14
|
+
return undefined;
|
|
15
|
+
return [workspace];
|
|
16
|
+
}, [navGroups]);
|
|
17
|
+
const navigationGroups = useMemo(() => {
|
|
18
|
+
if (workspaceGroups) {
|
|
19
|
+
const space = workspaceGroups[0];
|
|
20
|
+
const root = space.items.find((item) => item.to === space.focusOn) ?? space.items[0];
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
...space,
|
|
24
|
+
label: "Spaces",
|
|
25
|
+
icon: Layers,
|
|
26
|
+
priority: 1000,
|
|
27
|
+
items: root
|
|
28
|
+
? [{ ...root, children: root.childrenHidden ? root.children : undefined }]
|
|
29
|
+
: space.items,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "Content",
|
|
33
|
+
tree: true,
|
|
34
|
+
hideLabel: true,
|
|
35
|
+
priority: 800,
|
|
36
|
+
items: root?.childrenHidden ? [] : (root?.children ?? []),
|
|
37
|
+
meta: { hideInCollapsedRail: true },
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
...navGroups
|
|
43
|
+
.filter((group) => !["recent-chats", "pinned-chats"].includes(String(group.meta?.type)))
|
|
44
|
+
.map((group) => group.meta?.type === "spaces-workspaces"
|
|
45
|
+
? { ...group, label: "Spaces", icon: Layers }
|
|
46
|
+
: group.label === "Admin"
|
|
47
|
+
? { ...group, icon: Settings }
|
|
48
|
+
: group.label === "Activity"
|
|
49
|
+
? { ...group, priority: 1100 }
|
|
50
|
+
: group),
|
|
51
|
+
];
|
|
52
|
+
}, [workspaceGroups, navGroups]);
|
|
53
|
+
const staticNavigationGroups = useMemo(() => navigationGroups.map((group) => ({ ...group, collapsible: false })), [navigationGroups]);
|
|
54
|
+
return { navGroups, workspaceGroups, staticNavigationGroups };
|
|
55
|
+
}
|
|
56
|
+
export function useWorkbenchState(chatRoute) {
|
|
57
|
+
const panelOpen = useToolPanelStore((s) => s.isOpen && !s.isWindowHosted);
|
|
58
|
+
const chatOnly = chatRoute && !panelOpen;
|
|
59
|
+
const location = useLocation();
|
|
60
|
+
const pageLabel = useAppStore((s) => s.navContext.label);
|
|
61
|
+
const chatWidth = useChatWidth();
|
|
62
|
+
const [desktopCollapsed, setCollapsed] = useState(false);
|
|
63
|
+
const collapsed = !chatWidth.compact && desktopCollapsed;
|
|
64
|
+
const [compactPane, setCompactPane] = useState(chatOnly ? "chat" : "content");
|
|
65
|
+
const [contentCollapsed, setContentCollapsed] = useState(false);
|
|
66
|
+
const contentHidden = chatOnly || (chatWidth.compact ? compactPane !== "content" : contentCollapsed);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
setContentCollapsed(chatOnly);
|
|
69
|
+
setCompactPane(chatOnly ? "chat" : "content");
|
|
70
|
+
if (chatOnly)
|
|
71
|
+
setCollapsed(false);
|
|
72
|
+
// Navigation is an explicit request to see content, including selecting
|
|
73
|
+
// the same address again. Collapsing remains local until the next entry.
|
|
74
|
+
}, [chatOnly, location.key]);
|
|
75
|
+
const [historyOpen, setHistoryOpen] = useState(false);
|
|
76
|
+
const emptyChat = useAuiState((s) => s.thread.messages.length === 0 && !s.thread.isRunning && !s.thread.isLoading);
|
|
77
|
+
const wasEmptyChat = useRef(emptyChat);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (wasEmptyChat.current && !emptyChat)
|
|
80
|
+
setHistoryOpen(false);
|
|
81
|
+
wasEmptyChat.current = emptyChat;
|
|
82
|
+
}, [emptyChat]);
|
|
83
|
+
const showHistory = historyOpen;
|
|
84
|
+
const spaciousChat = chatOnly || contentCollapsed;
|
|
85
|
+
const returnToChat = () => {
|
|
86
|
+
setHistoryOpen(false);
|
|
87
|
+
};
|
|
88
|
+
const openHistory = () => {
|
|
89
|
+
setHistoryOpen(true);
|
|
90
|
+
};
|
|
91
|
+
const historyShortcut = (_jsxs(Button, { variant: "ghost", size: "sm", onClick: openHistory, className: "h-8 w-fit gap-2 px-3 text-xs font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(History, { className: "size-3.5", "aria-hidden": true }), "Conversation history"] }));
|
|
92
|
+
const toggleNavigation = useSidebarStore((s) => s.toggleMobile);
|
|
93
|
+
const { navGroups, workspaceGroups, staticNavigationGroups } = useWorkbenchNavigation();
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (panelOpen) {
|
|
96
|
+
setContentCollapsed(false);
|
|
97
|
+
setCompactPane("content");
|
|
98
|
+
}
|
|
99
|
+
}, [panelOpen]);
|
|
100
|
+
const closePanel = useToolPanelStore((s) => s.closePanel);
|
|
101
|
+
const requestedChat = useChatBubbleStore((s) => s.isOpen || s.isExpanded);
|
|
102
|
+
const newConversation = useNewConversation({
|
|
103
|
+
navigateToChat: chatRoute,
|
|
104
|
+
preserveNavigation: !chatRoute,
|
|
105
|
+
});
|
|
106
|
+
const startChat = () => {
|
|
107
|
+
setHistoryOpen(false);
|
|
108
|
+
setCompactPane("chat");
|
|
109
|
+
newConversation();
|
|
110
|
+
};
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (requestedChat) {
|
|
113
|
+
setHistoryOpen(false);
|
|
114
|
+
setCollapsed(false);
|
|
115
|
+
setCompactPane("chat");
|
|
116
|
+
useChatBubbleStore.getState().close();
|
|
117
|
+
}
|
|
118
|
+
}, [requestedChat]);
|
|
119
|
+
return {
|
|
120
|
+
panelOpen,
|
|
121
|
+
chatOnly,
|
|
122
|
+
pageLabel,
|
|
123
|
+
chatWidth,
|
|
124
|
+
collapsed,
|
|
125
|
+
setCollapsed,
|
|
126
|
+
compactPane,
|
|
127
|
+
setCompactPane,
|
|
128
|
+
contentCollapsed,
|
|
129
|
+
setContentCollapsed,
|
|
130
|
+
contentHidden,
|
|
131
|
+
historyOpen,
|
|
132
|
+
emptyChat,
|
|
133
|
+
showHistory,
|
|
134
|
+
spaciousChat,
|
|
135
|
+
returnToChat,
|
|
136
|
+
openHistory,
|
|
137
|
+
historyShortcut,
|
|
138
|
+
toggleNavigation,
|
|
139
|
+
navGroups,
|
|
140
|
+
workspaceGroups,
|
|
141
|
+
staticNavigationGroups,
|
|
142
|
+
closePanel,
|
|
143
|
+
startChat,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { useNavStore } from "../index.js";
|
|
3
|
+
export declare function WelcomeIntro({ navGroups, historyShortcut, }: {
|
|
4
|
+
navGroups: ReturnType<typeof useNavStore.getState>["config"];
|
|
5
|
+
historyShortcut: ReactNode;
|
|
6
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
3
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "../index.js";
|
|
4
|
+
import { Layers } from "lucide-react";
|
|
5
|
+
export function WelcomeIntro({ navGroups, historyShortcut, }) {
|
|
6
|
+
return (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: "What would you like to get done?" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: "Find information, create or edit content, and run actions across your spaces." }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: [historyShortcut, navGroups.some((group) => group.meta?.type === "spaces-workspaces" && group.createActions?.length) && (_jsx("div", { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "h-8 gap-2 px-3 text-xs font-normal text-muted-foreground [@media(pointer:coarse)]:min-h-11", children: [_jsx(Layers, { className: "size-3.5", "aria-hidden": "true" }), "Create a space"] }) }), _jsx(DropdownMenuContent, { align: "center", children: navGroups
|
|
7
|
+
.filter((group) => group.meta?.type === "spaces-workspaces")
|
|
8
|
+
.flatMap((group) => group.createActions ?? [])
|
|
9
|
+
.map((action) => (_jsx(DropdownMenuItem, { onSelect: () => action.handler(), children: action.label }, action.id))) })] }) }))] })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function WorkbenchStyles(): import("react/jsx-runtime").JSX.Element;
|