@getdom/studio 0.1.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/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import { computed, reactive } from 'vue';
|
|
2
|
+
import { defineStore } from 'pinia';
|
|
3
|
+
import { createAgentChatMockApi } from './agentChatMockApi.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL = '/api/agent-chat';
|
|
6
|
+
let logCounter = 0;
|
|
7
|
+
|
|
8
|
+
export const useAgentChatDemoStore = defineAgentChatStore('agent-chat-demo', {
|
|
9
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
10
|
+
api: createAgentChatMockApi(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a configured Pinia store for agent chat workflows.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} storeId Pinia store id.
|
|
17
|
+
* @param {Record<string, unknown>} options Store configuration.
|
|
18
|
+
* @returns {Function} Pinia use-store function.
|
|
19
|
+
*/
|
|
20
|
+
export function defineAgentChatStore(storeId = 'agent-chat', options = {}) {
|
|
21
|
+
return defineStore(storeId, () => createAgentChatStore(options));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a fetch-backed API adapter matching the demo API contract.
|
|
26
|
+
*
|
|
27
|
+
* @param {Record<string, unknown>} config API configuration.
|
|
28
|
+
* @returns {Record<string, Function>} API adapter.
|
|
29
|
+
*/
|
|
30
|
+
export function createAgentChatRestApi(config = {}) {
|
|
31
|
+
const baseUrl = String(config.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Requests JSON and throws for non-2xx responses.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} path Request path.
|
|
37
|
+
* @param {RequestInit} init Fetch options.
|
|
38
|
+
* @returns {Promise<Record<string, unknown>>} Parsed JSON response.
|
|
39
|
+
*/
|
|
40
|
+
async function requestJson(path, init = {}) {
|
|
41
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
...(init.headers || {}),
|
|
45
|
+
},
|
|
46
|
+
...init,
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw new Error(`Agent chat request failed with ${response.status}.`);
|
|
49
|
+
return response.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
bootstrap: () => requestJson('/bootstrap'),
|
|
54
|
+
getConversation: (request) => requestJson(`/conversations/${encodeURIComponent(request.conversationId)}`),
|
|
55
|
+
createConversation: (request) => requestJson('/conversations', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
body: JSON.stringify(request || {}),
|
|
58
|
+
}),
|
|
59
|
+
streamMessage: (request) => requestJson(`/conversations/${encodeURIComponent(request.conversationId)}/messages`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
signal: request.signal,
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
content: request.content,
|
|
64
|
+
mode: request.mode,
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates the Pinia setup store used by the agent chat docs and integrations.
|
|
72
|
+
*
|
|
73
|
+
* @param {Record<string, unknown>} options Store configuration.
|
|
74
|
+
* @returns {Record<string, unknown>} Store state, getters, and actions.
|
|
75
|
+
*/
|
|
76
|
+
function createAgentChatStore(options = {}) {
|
|
77
|
+
const config = normalizeStoreOptions(options);
|
|
78
|
+
const api = config.api || createAgentChatRestApi(config);
|
|
79
|
+
const state = reactive(createInitialState(config));
|
|
80
|
+
let activeController = null;
|
|
81
|
+
let activeStreamId = 0;
|
|
82
|
+
|
|
83
|
+
const agents = computed(() => state.agents);
|
|
84
|
+
const conversations = computed(() => state.conversations);
|
|
85
|
+
const activeConversationId = computed(() => state.activeConversationId);
|
|
86
|
+
const activeConversation = computed(() => state.conversations.find((conversation) => conversation.id === state.activeConversationId) || null);
|
|
87
|
+
const activeMessages = computed(() => state.messagesByConversationId[state.activeConversationId] || []);
|
|
88
|
+
const activeAgent = computed(() => state.agents.find((agent) => agent.id === activeConversation.value?.agentId) || null);
|
|
89
|
+
const input = computed(() => state.input);
|
|
90
|
+
const mode = computed(() => state.mode);
|
|
91
|
+
const search = computed(() => state.search);
|
|
92
|
+
const loading = computed(() => state.loading);
|
|
93
|
+
const streaming = computed(() => state.streaming);
|
|
94
|
+
const sidebarOpen = computed(() => state.sidebarOpen);
|
|
95
|
+
const asideOpen = computed(() => state.asideOpen);
|
|
96
|
+
const sidebarSize = computed(() => state.sidebarSize);
|
|
97
|
+
const asideSize = computed(() => state.asideSize);
|
|
98
|
+
const requestLog = computed(() => state.requestLog);
|
|
99
|
+
const lastRequest = computed(() => state.lastRequest);
|
|
100
|
+
const lastResponse = computed(() => state.lastResponse);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Loads agents and conversation summaries from the API.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<void>} Resolves after bootstrap data is applied.
|
|
106
|
+
*/
|
|
107
|
+
async function load() {
|
|
108
|
+
const log = logRequest(state, 'GET', `${config.baseUrl}/bootstrap`);
|
|
109
|
+
state.loading = true;
|
|
110
|
+
try {
|
|
111
|
+
const response = await api.bootstrap();
|
|
112
|
+
state.agents = Array.isArray(response.agents) ? response.agents : [];
|
|
113
|
+
state.conversations = Array.isArray(response.conversations) ? response.conversations : [];
|
|
114
|
+
if (!state.activeConversationId && state.conversations[0]) {
|
|
115
|
+
state.activeConversationId = state.conversations[0].id;
|
|
116
|
+
}
|
|
117
|
+
logResponse(state, log.id, response);
|
|
118
|
+
if (state.activeConversationId) await loadConversation(state.activeConversationId);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logError(state, log.id, error);
|
|
121
|
+
} finally {
|
|
122
|
+
state.loading = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Loads the full message list for a conversation.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} conversationId Conversation id.
|
|
130
|
+
* @returns {Promise<void>} Resolves after messages are applied.
|
|
131
|
+
*/
|
|
132
|
+
async function loadConversation(conversationId) {
|
|
133
|
+
if (!conversationId) return;
|
|
134
|
+
const log = logRequest(state, 'GET', `${config.baseUrl}/conversations/${conversationId}`, { conversationId });
|
|
135
|
+
state.loading = true;
|
|
136
|
+
try {
|
|
137
|
+
const response = await api.getConversation({ conversationId });
|
|
138
|
+
upsertConversation(state, response.conversation);
|
|
139
|
+
state.messagesByConversationId[conversationId] = Array.isArray(response.messages) ? response.messages : [];
|
|
140
|
+
logResponse(state, log.id, response);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logError(state, log.id, error);
|
|
143
|
+
} finally {
|
|
144
|
+
state.loading = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Selects a conversation and loads messages when needed.
|
|
150
|
+
*
|
|
151
|
+
* @param {string|Record<string, unknown>} conversation Conversation id or row.
|
|
152
|
+
* @returns {Promise<void>} Resolves after selection work completes.
|
|
153
|
+
*/
|
|
154
|
+
async function selectConversation(conversation) {
|
|
155
|
+
const conversationId = typeof conversation === 'string' ? conversation : conversation?.id;
|
|
156
|
+
if (!conversationId || conversationId === state.activeConversationId) return;
|
|
157
|
+
state.activeConversationId = conversationId;
|
|
158
|
+
if (!state.messagesByConversationId[conversationId]) await loadConversation(conversationId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Creates and selects a new conversation.
|
|
163
|
+
*
|
|
164
|
+
* @param {Record<string, unknown>} payload Create payload.
|
|
165
|
+
* @returns {Promise<Record<string, unknown>|null>} Created conversation.
|
|
166
|
+
*/
|
|
167
|
+
async function createConversation(payload = {}) {
|
|
168
|
+
const request = {
|
|
169
|
+
title: payload.title || 'New agent thread',
|
|
170
|
+
agentId: payload.agentId || activeAgent.value?.id || state.agents[0]?.id,
|
|
171
|
+
};
|
|
172
|
+
const log = logRequest(state, 'POST', `${config.baseUrl}/conversations`, request);
|
|
173
|
+
try {
|
|
174
|
+
const response = await api.createConversation(request);
|
|
175
|
+
upsertConversation(state, response.conversation, true);
|
|
176
|
+
state.messagesByConversationId[response.conversation.id] = Array.isArray(response.messages) ? response.messages : [];
|
|
177
|
+
state.activeConversationId = response.conversation.id;
|
|
178
|
+
state.input = '';
|
|
179
|
+
logResponse(state, log.id, response);
|
|
180
|
+
return response.conversation;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
logError(state, log.id, error);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Sends a user message and applies streamed assistant updates.
|
|
189
|
+
*
|
|
190
|
+
* @param {{ content?: string, mode?: 'text'|'audio' }} payload Submit payload.
|
|
191
|
+
* @returns {Promise<void>} Resolves when the stream finishes or stops.
|
|
192
|
+
*/
|
|
193
|
+
async function sendMessage(payload = {}) {
|
|
194
|
+
const content = String(payload.content || state.input || '').trim();
|
|
195
|
+
if (!content || state.streaming) return;
|
|
196
|
+
|
|
197
|
+
if (!state.activeConversationId) {
|
|
198
|
+
const created = await createConversation({ title: titleFromPrompt(content) });
|
|
199
|
+
if (!created) return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const conversationId = state.activeConversationId;
|
|
203
|
+
const userMessage = createLocalUserMessage(content, payload.mode || state.mode);
|
|
204
|
+
appendMessage(state, conversationId, userMessage);
|
|
205
|
+
state.input = '';
|
|
206
|
+
state.streaming = true;
|
|
207
|
+
patchConversation(state, conversationId, {
|
|
208
|
+
status: 'streaming',
|
|
209
|
+
mode: payload.mode || state.mode,
|
|
210
|
+
updatedAt: new Date().toISOString(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
activeController = typeof AbortController === 'undefined' ? null : new AbortController();
|
|
214
|
+
const streamId = activeStreamId + 1;
|
|
215
|
+
activeStreamId = streamId;
|
|
216
|
+
state.activeStreamId = streamId;
|
|
217
|
+
|
|
218
|
+
const request = {
|
|
219
|
+
conversationId,
|
|
220
|
+
content,
|
|
221
|
+
mode: payload.mode || state.mode,
|
|
222
|
+
userMessage,
|
|
223
|
+
signal: activeController?.signal,
|
|
224
|
+
};
|
|
225
|
+
const log = logRequest(state, 'POST', `${config.baseUrl}/conversations/${conversationId}/messages`, {
|
|
226
|
+
conversationId,
|
|
227
|
+
content,
|
|
228
|
+
mode: request.mode,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const response = await api.streamMessage(request, {
|
|
233
|
+
onAssistantStart: (message) => upsertStreamMessage(state, streamId, conversationId, message),
|
|
234
|
+
onPart: (messageId, part) => upsertStreamPart(state, streamId, conversationId, messageId, part),
|
|
235
|
+
onTextDelta: (messageId, partId, chunk) => appendStreamText(state, streamId, conversationId, messageId, partId, chunk),
|
|
236
|
+
onAssistantDone: (message) => upsertStreamMessage(state, streamId, conversationId, message),
|
|
237
|
+
});
|
|
238
|
+
if (streamId !== activeStreamId) return;
|
|
239
|
+
upsertConversation(state, response.conversation);
|
|
240
|
+
upsertMessage(state, conversationId, response.message);
|
|
241
|
+
logResponse(state, log.id, response);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error?.name === 'AbortError') {
|
|
244
|
+
markActiveStreamCancelled(state, conversationId);
|
|
245
|
+
logResponse(state, log.id, { cancelled: true });
|
|
246
|
+
} else {
|
|
247
|
+
patchConversation(state, conversationId, { status: 'error' });
|
|
248
|
+
logError(state, log.id, error);
|
|
249
|
+
}
|
|
250
|
+
} finally {
|
|
251
|
+
if (streamId === activeStreamId) {
|
|
252
|
+
state.streaming = false;
|
|
253
|
+
state.activeStreamId = 0;
|
|
254
|
+
activeController = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Stops the active streaming response.
|
|
261
|
+
*
|
|
262
|
+
* @returns {void}
|
|
263
|
+
*/
|
|
264
|
+
function stopStreaming() {
|
|
265
|
+
activeController?.abort();
|
|
266
|
+
activeController = null;
|
|
267
|
+
state.streaming = false;
|
|
268
|
+
activeStreamId += 1;
|
|
269
|
+
state.activeStreamId = activeStreamId;
|
|
270
|
+
if (state.activeConversationId) markActiveStreamCancelled(state, state.activeConversationId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Updates the composer input value.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} value Composer value.
|
|
277
|
+
* @returns {void}
|
|
278
|
+
*/
|
|
279
|
+
function setInput(value) {
|
|
280
|
+
state.input = value;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Updates the active input mode.
|
|
285
|
+
*
|
|
286
|
+
* @param {'text'|'audio'} value Input mode.
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
function setMode(value) {
|
|
290
|
+
state.mode = value === 'audio' ? 'audio' : 'text';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Updates the conversation search value.
|
|
295
|
+
*
|
|
296
|
+
* @param {string} value Search value.
|
|
297
|
+
* @returns {void}
|
|
298
|
+
*/
|
|
299
|
+
function setSearch(value) {
|
|
300
|
+
state.search = value;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Updates sidebar visibility.
|
|
305
|
+
*
|
|
306
|
+
* @param {boolean} value Visibility state.
|
|
307
|
+
* @returns {void}
|
|
308
|
+
*/
|
|
309
|
+
function setSidebarOpen(value) {
|
|
310
|
+
state.sidebarOpen = Boolean(value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Updates aside visibility.
|
|
315
|
+
*
|
|
316
|
+
* @param {boolean} value Visibility state.
|
|
317
|
+
* @returns {void}
|
|
318
|
+
*/
|
|
319
|
+
function setAsideOpen(value) {
|
|
320
|
+
state.asideOpen = Boolean(value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Updates sidebar splitter width.
|
|
325
|
+
*
|
|
326
|
+
* @param {number} value Width in pixels.
|
|
327
|
+
* @returns {void}
|
|
328
|
+
*/
|
|
329
|
+
function setSidebarSize(value) {
|
|
330
|
+
state.sidebarSize = clampNumber(value, 220, 520);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Updates aside splitter width.
|
|
335
|
+
*
|
|
336
|
+
* @param {number} value Width in pixels.
|
|
337
|
+
* @returns {void}
|
|
338
|
+
*/
|
|
339
|
+
function setAsideSize(value) {
|
|
340
|
+
state.asideSize = clampNumber(value, 260, 560);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Records a tool action in the demo log.
|
|
345
|
+
*
|
|
346
|
+
* @param {Record<string, unknown>} payload Tool action payload.
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
function handleToolAction(payload) {
|
|
350
|
+
const log = logRequest(state, 'UI', 'tool-action', payload);
|
|
351
|
+
logResponse(state, log.id, { accepted: true });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
agents,
|
|
356
|
+
conversations,
|
|
357
|
+
activeConversationId,
|
|
358
|
+
activeConversation,
|
|
359
|
+
activeMessages,
|
|
360
|
+
activeAgent,
|
|
361
|
+
input,
|
|
362
|
+
mode,
|
|
363
|
+
search,
|
|
364
|
+
loading,
|
|
365
|
+
streaming,
|
|
366
|
+
sidebarOpen,
|
|
367
|
+
asideOpen,
|
|
368
|
+
sidebarSize,
|
|
369
|
+
asideSize,
|
|
370
|
+
requestLog,
|
|
371
|
+
lastRequest,
|
|
372
|
+
lastResponse,
|
|
373
|
+
load,
|
|
374
|
+
loadConversation,
|
|
375
|
+
selectConversation,
|
|
376
|
+
createConversation,
|
|
377
|
+
sendMessage,
|
|
378
|
+
stopStreaming,
|
|
379
|
+
setInput,
|
|
380
|
+
setMode,
|
|
381
|
+
setSearch,
|
|
382
|
+
setSidebarOpen,
|
|
383
|
+
setAsideOpen,
|
|
384
|
+
setSidebarSize,
|
|
385
|
+
setAsideSize,
|
|
386
|
+
handleToolAction,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Normalizes store options.
|
|
392
|
+
*
|
|
393
|
+
* @param {Record<string, unknown>} options Store options.
|
|
394
|
+
* @returns {Record<string, unknown>} Normalized options.
|
|
395
|
+
*/
|
|
396
|
+
function normalizeStoreOptions(options = {}) {
|
|
397
|
+
return {
|
|
398
|
+
baseUrl: options.baseUrl || DEFAULT_BASE_URL,
|
|
399
|
+
api: options.api || null,
|
|
400
|
+
sidebarOpen: options.sidebarOpen !== false,
|
|
401
|
+
asideOpen: options.asideOpen !== false,
|
|
402
|
+
sidebarSize: Number(options.sidebarSize || 300),
|
|
403
|
+
asideSize: Number(options.asideSize || 340),
|
|
404
|
+
mode: options.mode === 'audio' ? 'audio' : 'text',
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Creates the initial reactive store state.
|
|
410
|
+
*
|
|
411
|
+
* @param {Record<string, unknown>} config Normalized config.
|
|
412
|
+
* @returns {Record<string, unknown>} Store state.
|
|
413
|
+
*/
|
|
414
|
+
function createInitialState(config) {
|
|
415
|
+
return {
|
|
416
|
+
agents: [],
|
|
417
|
+
conversations: [],
|
|
418
|
+
messagesByConversationId: {},
|
|
419
|
+
activeConversationId: '',
|
|
420
|
+
input: '',
|
|
421
|
+
mode: config.mode,
|
|
422
|
+
search: '',
|
|
423
|
+
loading: false,
|
|
424
|
+
streaming: false,
|
|
425
|
+
activeStreamId: 0,
|
|
426
|
+
sidebarOpen: config.sidebarOpen,
|
|
427
|
+
asideOpen: config.asideOpen,
|
|
428
|
+
sidebarSize: config.sidebarSize,
|
|
429
|
+
asideSize: config.asideSize,
|
|
430
|
+
requestLog: [],
|
|
431
|
+
lastRequest: null,
|
|
432
|
+
lastResponse: null,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Creates a local user message for optimistic rendering.
|
|
438
|
+
*
|
|
439
|
+
* @param {string} content User content.
|
|
440
|
+
* @param {'text'|'audio'} mode Message mode.
|
|
441
|
+
* @returns {Record<string, unknown>} Message record.
|
|
442
|
+
*/
|
|
443
|
+
function createLocalUserMessage(content, mode = 'text') {
|
|
444
|
+
const message = {
|
|
445
|
+
id: `local_user_${Date.now()}`,
|
|
446
|
+
role: 'user',
|
|
447
|
+
content,
|
|
448
|
+
status: 'complete',
|
|
449
|
+
mode,
|
|
450
|
+
createdAt: new Date().toISOString(),
|
|
451
|
+
};
|
|
452
|
+
if (mode === 'audio') {
|
|
453
|
+
message.parts = [
|
|
454
|
+
{
|
|
455
|
+
id: `local_audio_${Date.now()}`,
|
|
456
|
+
type: 'audio',
|
|
457
|
+
label: 'Audio message',
|
|
458
|
+
status: 'complete',
|
|
459
|
+
text: `Transcript: ${content}`,
|
|
460
|
+
},
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
return message;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Inserts or updates a conversation record.
|
|
468
|
+
*
|
|
469
|
+
* @param {Record<string, unknown>} state Store state.
|
|
470
|
+
* @param {Record<string, unknown>} conversation Conversation record.
|
|
471
|
+
* @param {boolean} prepend Whether to place new records first.
|
|
472
|
+
* @returns {void}
|
|
473
|
+
*/
|
|
474
|
+
function upsertConversation(state, conversation, prepend = false) {
|
|
475
|
+
if (!conversation?.id) return;
|
|
476
|
+
const index = state.conversations.findIndex((item) => item.id === conversation.id);
|
|
477
|
+
if (index >= 0) {
|
|
478
|
+
state.conversations[index] = {
|
|
479
|
+
...state.conversations[index],
|
|
480
|
+
...conversation,
|
|
481
|
+
};
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (prepend) state.conversations.unshift(conversation);
|
|
485
|
+
else state.conversations.push(conversation);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Applies a partial update to a conversation.
|
|
490
|
+
*
|
|
491
|
+
* @param {Record<string, unknown>} state Store state.
|
|
492
|
+
* @param {string} conversationId Conversation id.
|
|
493
|
+
* @param {Record<string, unknown>} patch Partial fields.
|
|
494
|
+
* @returns {void}
|
|
495
|
+
*/
|
|
496
|
+
function patchConversation(state, conversationId, patch) {
|
|
497
|
+
const conversation = state.conversations.find((item) => item.id === conversationId);
|
|
498
|
+
if (!conversation) return;
|
|
499
|
+
Object.assign(conversation, patch);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Appends a message to a conversation.
|
|
504
|
+
*
|
|
505
|
+
* @param {Record<string, unknown>} state Store state.
|
|
506
|
+
* @param {string} conversationId Conversation id.
|
|
507
|
+
* @param {Record<string, unknown>} message Message record.
|
|
508
|
+
* @returns {void}
|
|
509
|
+
*/
|
|
510
|
+
function appendMessage(state, conversationId, message) {
|
|
511
|
+
const messages = ensureMessages(state, conversationId);
|
|
512
|
+
if (!messages.some((item) => item.id === message.id)) messages.push(message);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Inserts or updates a message for a conversation.
|
|
517
|
+
*
|
|
518
|
+
* @param {Record<string, unknown>} state Store state.
|
|
519
|
+
* @param {string} conversationId Conversation id.
|
|
520
|
+
* @param {Record<string, unknown>} message Message record.
|
|
521
|
+
* @returns {void}
|
|
522
|
+
*/
|
|
523
|
+
function upsertMessage(state, conversationId, message) {
|
|
524
|
+
if (!message?.id) return;
|
|
525
|
+
const messages = ensureMessages(state, conversationId);
|
|
526
|
+
const index = messages.findIndex((item) => item.id === message.id);
|
|
527
|
+
if (index >= 0) {
|
|
528
|
+
messages[index] = {
|
|
529
|
+
...messages[index],
|
|
530
|
+
...message,
|
|
531
|
+
};
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
messages.push(message);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Applies a streamed message when the stream is still current.
|
|
539
|
+
*
|
|
540
|
+
* @param {Record<string, unknown>} state Store state.
|
|
541
|
+
* @param {number} streamId Stream id.
|
|
542
|
+
* @param {string} conversationId Conversation id.
|
|
543
|
+
* @param {Record<string, unknown>} message Message record.
|
|
544
|
+
* @returns {void}
|
|
545
|
+
*/
|
|
546
|
+
function upsertStreamMessage(state, streamId, conversationId, message) {
|
|
547
|
+
if (!isCurrentStream(state, streamId)) return;
|
|
548
|
+
upsertMessage(state, conversationId, message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Inserts or updates a streamed message part.
|
|
553
|
+
*
|
|
554
|
+
* @param {Record<string, unknown>} state Store state.
|
|
555
|
+
* @param {number} streamId Stream id.
|
|
556
|
+
* @param {string} conversationId Conversation id.
|
|
557
|
+
* @param {string} messageId Message id.
|
|
558
|
+
* @param {Record<string, unknown>} part Message part.
|
|
559
|
+
* @returns {void}
|
|
560
|
+
*/
|
|
561
|
+
function upsertStreamPart(state, streamId, conversationId, messageId, part) {
|
|
562
|
+
if (!isCurrentStream(state, streamId)) return;
|
|
563
|
+
const message = findMessage(state, conversationId, messageId);
|
|
564
|
+
if (!message) return;
|
|
565
|
+
if (!Array.isArray(message.parts)) message.parts = [];
|
|
566
|
+
const index = message.parts.findIndex((item) => item.id === part.id);
|
|
567
|
+
if (index >= 0) message.parts[index] = { ...message.parts[index], ...part };
|
|
568
|
+
else message.parts.push(part);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Appends streamed text to a text part.
|
|
573
|
+
*
|
|
574
|
+
* @param {Record<string, unknown>} state Store state.
|
|
575
|
+
* @param {number} streamId Stream id.
|
|
576
|
+
* @param {string} conversationId Conversation id.
|
|
577
|
+
* @param {string} messageId Message id.
|
|
578
|
+
* @param {string} partId Part id.
|
|
579
|
+
* @param {string} chunk Text chunk.
|
|
580
|
+
* @returns {void}
|
|
581
|
+
*/
|
|
582
|
+
function appendStreamText(state, streamId, conversationId, messageId, partId, chunk) {
|
|
583
|
+
if (!isCurrentStream(state, streamId)) return;
|
|
584
|
+
const message = findMessage(state, conversationId, messageId);
|
|
585
|
+
if (!message) return;
|
|
586
|
+
const part = (message.parts || []).find((item) => item.id === partId);
|
|
587
|
+
if (!part) return;
|
|
588
|
+
part.text = `${part.text || ''}${chunk}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Marks the active assistant message and conversation as cancelled.
|
|
593
|
+
*
|
|
594
|
+
* @param {Record<string, unknown>} state Store state.
|
|
595
|
+
* @param {string} conversationId Conversation id.
|
|
596
|
+
* @returns {void}
|
|
597
|
+
*/
|
|
598
|
+
function markActiveStreamCancelled(state, conversationId) {
|
|
599
|
+
const messages = ensureMessages(state, conversationId);
|
|
600
|
+
const message = [...messages].reverse().find((item) => item.role === 'assistant' && item.status === 'streaming');
|
|
601
|
+
if (message) {
|
|
602
|
+
message.status = 'cancelled';
|
|
603
|
+
for (const part of Array.isArray(message.parts) ? message.parts : []) {
|
|
604
|
+
if (part.status === 'running' || part.status === 'streaming') part.status = 'cancelled';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
patchConversation(state, conversationId, { status: 'idle' });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Checks whether a stream id is still current.
|
|
612
|
+
*
|
|
613
|
+
* @param {Record<string, unknown>} state Store state.
|
|
614
|
+
* @param {number} streamId Stream id.
|
|
615
|
+
* @returns {boolean} True when the stream should mutate state.
|
|
616
|
+
*/
|
|
617
|
+
function isCurrentStream(state, streamId) {
|
|
618
|
+
return state.streaming && state.activeStreamId === streamId;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Finds a message by id.
|
|
623
|
+
*
|
|
624
|
+
* @param {Record<string, unknown>} state Store state.
|
|
625
|
+
* @param {string} conversationId Conversation id.
|
|
626
|
+
* @param {string} messageId Message id.
|
|
627
|
+
* @returns {Record<string, unknown>|undefined} Matching message.
|
|
628
|
+
*/
|
|
629
|
+
function findMessage(state, conversationId, messageId) {
|
|
630
|
+
return ensureMessages(state, conversationId).find((message) => message.id === messageId);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Ensures a conversation message array exists.
|
|
635
|
+
*
|
|
636
|
+
* @param {Record<string, unknown>} state Store state.
|
|
637
|
+
* @param {string} conversationId Conversation id.
|
|
638
|
+
* @returns {Array<Record<string, unknown>>} Message array.
|
|
639
|
+
*/
|
|
640
|
+
function ensureMessages(state, conversationId) {
|
|
641
|
+
if (!state.messagesByConversationId[conversationId]) state.messagesByConversationId[conversationId] = [];
|
|
642
|
+
return state.messagesByConversationId[conversationId];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Creates a request log entry.
|
|
647
|
+
*
|
|
648
|
+
* @param {Record<string, unknown>} state Store state.
|
|
649
|
+
* @param {string} method Request method.
|
|
650
|
+
* @param {string} url Request URL.
|
|
651
|
+
* @param {unknown} payload Request payload.
|
|
652
|
+
* @returns {Record<string, unknown>} Log entry.
|
|
653
|
+
*/
|
|
654
|
+
function logRequest(state, method, url, payload = null) {
|
|
655
|
+
logCounter += 1;
|
|
656
|
+
const entry = {
|
|
657
|
+
id: `agent_chat_log_${logCounter}`,
|
|
658
|
+
method,
|
|
659
|
+
url,
|
|
660
|
+
payload,
|
|
661
|
+
status: 'pending',
|
|
662
|
+
requestedAt: new Date().toISOString(),
|
|
663
|
+
response: null,
|
|
664
|
+
error: null,
|
|
665
|
+
};
|
|
666
|
+
state.requestLog.unshift(entry);
|
|
667
|
+
state.requestLog = state.requestLog.slice(0, 12);
|
|
668
|
+
state.lastRequest = entry;
|
|
669
|
+
return entry;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Marks a request log entry as successful.
|
|
674
|
+
*
|
|
675
|
+
* @param {Record<string, unknown>} state Store state.
|
|
676
|
+
* @param {string} id Log entry id.
|
|
677
|
+
* @param {unknown} response Response payload.
|
|
678
|
+
* @returns {void}
|
|
679
|
+
*/
|
|
680
|
+
function logResponse(state, id, response) {
|
|
681
|
+
const entry = state.requestLog.find((item) => item.id === id);
|
|
682
|
+
if (!entry) return;
|
|
683
|
+
entry.status = 'complete';
|
|
684
|
+
entry.response = response;
|
|
685
|
+
entry.completedAt = new Date().toISOString();
|
|
686
|
+
state.lastResponse = response;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Marks a request log entry as failed.
|
|
691
|
+
*
|
|
692
|
+
* @param {Record<string, unknown>} state Store state.
|
|
693
|
+
* @param {string} id Log entry id.
|
|
694
|
+
* @param {Error} error Error object.
|
|
695
|
+
* @returns {void}
|
|
696
|
+
*/
|
|
697
|
+
function logError(state, id, error) {
|
|
698
|
+
const entry = state.requestLog.find((item) => item.id === id);
|
|
699
|
+
if (!entry) return;
|
|
700
|
+
entry.status = 'error';
|
|
701
|
+
entry.error = error?.message || String(error);
|
|
702
|
+
entry.completedAt = new Date().toISOString();
|
|
703
|
+
state.lastResponse = { error: entry.error };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Creates a short title from a prompt.
|
|
708
|
+
*
|
|
709
|
+
* @param {string} prompt User prompt.
|
|
710
|
+
* @returns {string} Title text.
|
|
711
|
+
*/
|
|
712
|
+
function titleFromPrompt(prompt) {
|
|
713
|
+
const normalized = String(prompt || '').trim().replace(/\s+/g, ' ');
|
|
714
|
+
if (!normalized) return 'New agent thread';
|
|
715
|
+
return normalized.length > 42 ? `${normalized.slice(0, 39)}...` : normalized;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Clamps a number inside a range.
|
|
720
|
+
*
|
|
721
|
+
* @param {number} value Source number.
|
|
722
|
+
* @param {number} min Minimum value.
|
|
723
|
+
* @param {number} max Maximum value.
|
|
724
|
+
* @returns {number} Clamped value.
|
|
725
|
+
*/
|
|
726
|
+
function clampNumber(value, min, max) {
|
|
727
|
+
const number = Number(value);
|
|
728
|
+
if (!Number.isFinite(number)) return min;
|
|
729
|
+
return Math.min(Math.max(number, min), max);
|
|
730
|
+
}
|