From 10528769fa977022d67a1aae53be665207990219 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Wed, 26 Jul 2023 08:45:11 -0400 Subject: [PATCH 1/8] WIP on ChatGPT API integration testing --- client-app/package.json | 3 +- client-app/src/Bootstrap.ts | 2 + client-app/src/apps/chat.ts | 17 + client-app/src/core/svc/ChatGptService.ts | 292 ++++++++++ client-app/src/core/svc/PortfolioService.ts | 52 +- client-app/src/examples/chat/AppComponent.ts | 278 ++++++++++ client-app/src/examples/chat/AppModel.ts | 147 +++++ client-app/src/examples/chat/Chat.scss | 87 +++ .../src/examples/chat/system-message.md | 43 ++ client-app/yarn.lock | 509 +++++++++++++++++- .../xh/toolbox/admin/ChatGptController.groovy | 23 + .../xh/toolbox/portfolio/RawPosition.groovy | 1 + 12 files changed, 1441 insertions(+), 13 deletions(-) create mode 100644 client-app/src/apps/chat.ts create mode 100644 client-app/src/core/svc/ChatGptService.ts create mode 100644 client-app/src/examples/chat/AppComponent.ts create mode 100644 client-app/src/examples/chat/AppModel.ts create mode 100644 client-app/src/examples/chat/Chat.scss create mode 100644 client-app/src/examples/chat/system-message.md create mode 100644 grails-app/controllers/io/xh/toolbox/admin/ChatGptController.groovy diff --git a/client-app/package.json b/client-app/package.json index 1573bf9ff..64a45d8b0 100755 --- a/client-app/package.json +++ b/client-app/package.json @@ -49,7 +49,8 @@ "lodash": "4.x", "moment": "2.x", "react": "~18.2.0", - "react-dom": "~18.2.0" + "react-dom": "~18.2.0", + "react-markdown": "~8.0.7" }, "devDependencies": { "@xh/hoist-dev-utils": "7.x", diff --git a/client-app/src/Bootstrap.ts b/client-app/src/Bootstrap.ts index 722c639c0..e835f249f 100755 --- a/client-app/src/Bootstrap.ts +++ b/client-app/src/Bootstrap.ts @@ -10,6 +10,7 @@ import {XH} from '@xh/hoist/core'; import {when} from '@xh/hoist/mobx'; import {ContactService} from './examples/contact/svc/ContactService'; +import {ChatGptService} from './core/svc/ChatGptService'; import {GitHubService} from './core/svc/GitHubService'; import {PortfolioService} from './core/svc/PortfolioService'; import {OauthService} from './core/svc/OauthService'; @@ -18,6 +19,7 @@ import {TaskService} from './examples/todo/TaskService'; declare module '@xh/hoist/core' { // Merge interface with XHApi class to include injected services. export interface XHApi { + chatGptService: ChatGptService; contactService: ContactService; gitHubService: GitHubService; oauthService: OauthService; diff --git a/client-app/src/apps/chat.ts b/client-app/src/apps/chat.ts new file mode 100644 index 000000000..0c9c5243a --- /dev/null +++ b/client-app/src/apps/chat.ts @@ -0,0 +1,17 @@ +import '../Bootstrap'; + +import {XH} from '@xh/hoist/core'; +import {AppContainer} from '@xh/hoist/desktop/appcontainer'; +import {AppComponent} from '../examples/chat/AppComponent'; +import {AppModel} from '../examples/chat/AppModel'; + +XH.renderApp({ + clientAppCode: 'chat', + clientAppName: 'ChatGPT Labs', + componentClass: AppComponent, + modelClass: AppModel, + containerClass: AppContainer, + isMobileApp: false, + isSSO: true, + checkAccess: 'CHAT_GPT_USER' +}); diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts new file mode 100644 index 000000000..ff45636ac --- /dev/null +++ b/client-app/src/core/svc/ChatGptService.ts @@ -0,0 +1,292 @@ +import {HoistService, persist, XH} from '@xh/hoist/core'; +import {dropRight, isEmpty, isString, last, remove} from 'lodash'; +import {bindable, makeObservable, observable} from '@xh/hoist/mobx'; +import {logInfo, withInfo} from '@xh/hoist/utils/js'; + +export interface GptMessage { + role: 'system' | 'user' | 'assistant' | 'function'; + content?: string; + name?: string; + function_call?: GptFnCallResponse; +} + +export interface GptFnCallResponse { + // Name of the function to be called. + name: string; + // Args to pass to function, escaped JSON string + // ...or maybe a primitive if that's the function signature - need to check + arguments: string; +} + +export interface GptChatOptions { + model?: GptModel; + function_call?: GptFnCallRequest; +} + +export type GptFnCallRequest = 'none' | 'auto' | {name: string}; + +export type GptModel = 'gpt-3.5-turbo' | 'gpt-4'; + +export class ChatGptService extends HoistService { + override persistWith = {localStorageKey: 'chatGptService'}; + + // Initialized from config via dedicated server call. + // Configs are protected and not sent to all clients - the CHAT_GPT_USER role required. + apiKey: string; + completionUrl: string; + + /** + * Log of all messages sent back and forth between user and GPT. + * Sent with each new message to provide GPT with the overall conversation / context. + */ + @bindable.ref + @persist + messages: GptMessage[] = []; + + /** + * History of recent user messages to support quick re-selection by user. + * Persisted separately from the main message stream sent to GPT with each request. + */ + @bindable.ref + @persist + userMessageHistory: string[] = []; + + get userMessages(): GptMessage[] { + return this.messages.filter(it => it.role === 'user'); + } + + get systemMessage(): GptMessage { + return this.messages.find(it => it.role === 'system'); + } + + get hasMessages(): boolean { + return !isEmpty(this.messages); + } + + @bindable + @persist + model = 'gpt-3.5-turbo'; + selectableModels = ['gpt-3.5-turbo', 'gpt-4']; + + @observable isInitialized = false; + + @bindable + @persist + sendSystemMessage = true; + + initialSystemMessage = + 'You are a professional AI assistant embedded within a custom financial reporting dashboard application created by a\n' + + 'hedge fund with headquarters in the United States. Your role is to respond to user queries with either a function call\n' + + 'that the application can run OR a message asking the user to clarify or explaining why you are unable to help.\n' + + '\n' + + '### Objects returned and aggregated by the application API\n' + + '\n' + + 'The `getPortfolioPositions` function returns a list of `Position` objects. A `Position` satisfies the following\n' + + 'interface:\n' + + '\n' + + '```typescript\n' + + 'interface Position {\n' + + ' id: string;\n' + + ' name: string;\n' + + ' pnl: number;\n' + + ' mktVal: number;\n' + + ' children: Position[];\n' + + '}\n' + + '```\n' + + '\n' + + 'A `Position` represents an aggregate of one or more `RawPosition` objects. A `RawPosition` models a single investment\n' + + 'within a portfolio. It satisfies the following interface:\n' + + '\n' + + '```typescript\n' + + 'interface RawPosition {\n' + + " // Dimension - the stock ticker or identifier of the position's instrument, an equity stock or other security - e.g. ['AAPL', 'GOOG', 'MSFT']\n" + + ' symbol: string;\n' + + " // Dimension - the industry sector of the instrument - e.g. ['Technology', 'Healthcare', 'Energy']\n" + + ' sector: string;\n' + + " // Dimension - the name of an investment fund - e.g. ['Winter Star Fund', 'Oak Mount Fund']\n" + + ' fund: string;\n' + + " // Dimension - the name of the trader or portfolio manager responsible for the investment - e.g. ['Susan Major', 'Fred Corn', 'HedgeSys']\n" + + ' trader: string;\n' + + ' // Measure - the current value of the position, in USD.\n' + + ' mktVal: number;\n' + + ' // Measure - the current profit and loss of the position, in USD.\n' + + ' pnl: number;\n' + + '}\n' + + '```\n' + + '\n' + + 'The `getPortfolioPositions` function takes a list of `groupByDimensions` when aggregating results, representing\n' + + 'the field names of `RawPosition` dimensions within the portfolio data.\n' + + '\n' + + 'Introduce yourself to the user and ask them how you can help them.\n'; + + functions = [ + { + name: 'getPortfolioPositions', + description: + 'Query a portfolio of `RawPosition` objects representing investments to return aggregated `Position` objects with P&L (profit and loss) and market value data, grouped by one or more specified dimensions. Each grouped row in the return will have the following properties: `name`, `pnl` (profit and loss), and `mktVal` (market value). If multiple grouping dimensions are specified, the results will be returned in a tree structure, where each parent group will have a `children` property containing an array of nested sub-groups.', + parameters: { + type: 'object', + properties: { + groupByDimensions: { + description: + 'Array of one or more dimensions by which the portfolio positions should be aggregated.', + type: 'array', + items: { + type: 'string', + enum: ['fund', 'model', 'region', 'sector', 'trader', 'symbol'] + }, + minItems: 1, + uniqueItems: true + }, + sortBy: { + description: + 'The sort order of the returned results, by P&L or Market Value, either ascending or descending. Default is pnl|desc.', + type: 'string', + enum: ['pnl|desc', 'pnl|asc', 'mktVal|desc', 'mktVal|asc'] + }, + maxRows: { + description: + 'The maximum number of top-level rows to return. Leave unspecified to return all available groupings.', + type: 'integer', + minimum: 1 + } + }, + required: ['groupByDimensions'] + } + } + ]; + + constructor() { + super(); + makeObservable(this); + + this.messages.forEach(msg => this.logMsg(msg)); + + this.addReaction( + { + track: () => this.messages, + run: msgs => { + this.hasMessages ? this.logMsg(last(msgs)) : logInfo('Messages cleared', this); + } + }, + { + track: () => this.sendSystemMessage, + run: () => this.clearAndReInitAsync() + } + ); + } + + logMsg(msg: GptMessage) { + logInfo(`Received message: ${JSON.stringify(msg, null, 2)}`, this); + } + + override async initAsync() { + const conf = await XH.fetchJson({ + url: 'chatGpt/config' + }); + this.apiKey = conf.apiKey; + this.completionUrl = conf.completionUrl; + + if (!this.apiKey || !this.completionUrl) { + throw XH.exception('ChatGPT configuration is missing required values.'); + } + + const {systemMessage, sendSystemMessage, initialSystemMessage, hasMessages} = this; + if ( + (systemMessage && !sendSystemMessage) || + (systemMessage && systemMessage.content !== initialSystemMessage) || + (!systemMessage && hasMessages && sendSystemMessage) + ) { + this.clearHistory(); + XH.toast('System message has changed - history cleared.'); + } + + if (!this.hasMessages && this.sendSystemMessage) { + this.sendChatAsync({ + role: 'system', + content: this.initialSystemMessage + }) + .thenAction(() => (this.isInitialized = true)) + .catch(e => { + this.isInitialized = false; + XH.handleException(e, { + message: 'Failed to initialize ChatGPTService.', + alertType: 'toast' + }); + }); + } else { + this.isInitialized = true; + } + } + + // TODO - cancel any pending requests + async clearAndReInitAsync() { + this.clearHistory(); + await this.initAsync(); + } + + async sendChatAsync(message: GptMessage | string, options: GptChatOptions = {}) { + const msgToSend: GptMessage = isString(message) + ? { + role: 'user', + content: message + } + : message; + + // Push user message onto state immediately, to indicate that it's been sent. + this.messages = [...this.messages, msgToSend]; + + // And user messages to history for convenient re-selection. + if (msgToSend.role === 'user') { + this.updateUserMessageHistory(msgToSend.content); + } + + const body = { + model: this.model, + messages: this.messages, + functions: this.functions, + ...options + }; + + let resp; + try { + await withInfo('Called ChatGPT', async () => { + resp = await XH.fetchService.postJson({ + url: this.completionUrl, + headers: { + Authorization: `Bearer ${this.apiKey}` + }, + fetchOpts: {credentials: 'omit'}, + body + }); + }); + } catch (e) { + // Unwind user message - was not successfully posted. + this.messages = dropRight(this.messages); + throw e; + } + + console.debug(resp); + if (isEmpty(resp?.choices)) throw XH.exception('GPT did not return any choices'); + + const gptReply = resp.choices[0]; + this.messages = [...this.messages, gptReply.message]; + } + + updateUserMessageHistory(msg: string) { + const history = [...this.userMessageHistory]; + if (history.includes(msg)) { + remove(history, it => it === msg); + } + history.unshift(msg); + this.userMessageHistory = history; + } + + clearUserMessageHistory() { + this.userMessageHistory = []; + } + + clearHistory() { + this.messages = []; + } +} diff --git a/client-app/src/core/svc/PortfolioService.ts b/client-app/src/core/svc/PortfolioService.ts index ec56c733c..ce859f8ab 100644 --- a/client-app/src/core/svc/PortfolioService.ts +++ b/client-app/src/core/svc/PortfolioService.ts @@ -21,14 +21,13 @@ export class PortfolioService extends HoistService { * Return a portfolio of hierarchically grouped positions for the selected dimension(s). * @param dims - field names for dimensions on which to group. * @param includeSummary - true to include a root summary node - * @param maxPositions - truncate position tree, by smallest pnl, until this number of - * positions is reached. + * @param maxPositions - truncate position tree, by smallest pnl, until this number of positions is reached. */ async getPositionsAsync( dims: string[], includeSummary = false, maxPositions = this.MAX_POSITIONS - ): Promise { + ): Promise { const positions = await XH.fetchJson({ url: 'portfolio/positions', params: { @@ -42,10 +41,9 @@ export class PortfolioService extends HoistService { /** * Return a single grouped position, uniquely identified by drilldown ID. - * @param positionId - ID installed on each position returned by `getPositionsAsync()`. - * @return {Promise<*>} + * @param positionId - a {@see Position#id} */ - async getPositionAsync(positionId) { + async getPositionAsync(positionId): Promise { return XH.fetchJson({ url: 'portfolio/position', params: { @@ -77,15 +75,15 @@ export class PortfolioService extends HoistService { /** * Return a list of flat position data. */ - async getRawPositionsAsync({loadSpec}: any = {}): Promise { + async getRawPositionsAsync({loadSpec}: any = {}): Promise { return XH.fetchJson({url: 'portfolio/rawPositions', loadSpec}); } - async getAllOrdersAsync({loadSpec}: any = {}): Promise { + async getAllOrdersAsync({loadSpec}: any = {}): Promise { return XH.fetchJson({url: 'portfolio/orders', loadSpec}); } - async getOrdersAsync({positionId, loadSpec}): Promise { + async getOrdersAsync({positionId, loadSpec}): Promise { return XH.fetchJson({ url: 'portfolio/ordersForPosition', params: {positionId}, @@ -131,3 +129,39 @@ export class PortfolioService extends HoistService { }; } } + +export interface Position { + id: string; + name: string; + pnl: number; + mktVal: number; + children: Position[]; +} + +export interface RawPosition { + symbol: string; + model: string; + sector: string; + fund: string; + trader: string; + mktVal: number; + pnl: number; +} + +export interface Order { + id: string; + symbol: string; + sector: string; + region: string; + model: string; + trader: string; + fund: string; + dir: string; + quantity: number; + price: number; + mktVal: number; + time: number; + commission: number; + confidences: number; + closingPrices: number[]; +} diff --git a/client-app/src/examples/chat/AppComponent.ts b/client-app/src/examples/chat/AppComponent.ts new file mode 100644 index 000000000..de5a38aee --- /dev/null +++ b/client-app/src/examples/chat/AppComponent.ts @@ -0,0 +1,278 @@ +import {library} from '@fortawesome/fontawesome-svg-core'; +import {faPaperPlane, faRobot, faUserRobotXmarks} from '@fortawesome/pro-regular-svg-icons'; +import {elementFactory, hoistCmp, HoistProps, uses, XH} from '@xh/hoist/core'; +import {appBar, appBarSeparator} from '@xh/hoist/desktop/cmp/appbar'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {Icon} from '@xh/hoist/icon'; +import {AppModel} from './AppModel'; +import '../../core/Toolbox.scss'; +import {box, div, fragment, hbox, img, placeholder, vbox, vspacer} from '@xh/hoist/cmp/layout'; +import {jsonInput, select, switchInput, textArea} from '@xh/hoist/desktop/cmp/input'; +import {button} from '@xh/hoist/desktop/cmp/button'; +import {isEmpty} from 'lodash'; +import {GptMessage} from '../../core/svc/ChatGptService'; +import ReactMarkdown from 'react-markdown'; +import './Chat.scss'; +import {errorMessage} from '@xh/hoist/desktop/cmp/error'; +import {popover} from '@xh/hoist/kit/blueprint'; +import {grid} from '@xh/hoist/cmp/grid'; + +library.add(faPaperPlane, faRobot, faUserRobotXmarks); + +export const AppComponent = hoistCmp({ + displayName: 'App', + model: uses(AppModel), + + render() { + return panel({ + tbar: appBar({ + icon: Icon.icon({iconName: 'robot', size: '2x'}), + appMenuButtonProps: {hideLogoutItem: false}, + rightItems: [appBarControls()] + }), + items: XH.chatGptService.isInitialized + ? [msgList(), promptInput()] + : placeholder( + Icon.icon({iconName: 'user-robot-xmarks'}), + 'ChatGPTService not initialized.', + button({ + text: 'Retry', + onClick: () => XH.chatGptService.clearAndReInitAsync() + }) + ) + }); + } +}); + +const msgList = hoistCmp.factory({ + render({model}) { + const {messages} = XH.chatGptService, + item = isEmpty(messages) + ? placeholder(Icon.ellipsisHorizontal(), 'No messages yet...') + : div({ + className: 'tb-msg-list', + items: [ + ...messages.map(message => msgItem({message})), + // Supports scrolling to bottom of list. + box({ref: model.scrollRef}) + ] + }); + + return panel({ + item, + loadingIndicator: model.taskObserver + }); + } +}); + +const msgItem = hoistCmp.factory({ + render({model, message}) { + const {role, content, function_call} = message, + items = []; + + // System message is visible via popover from top toolbar. + if (role === 'system') return null; + + if (content) { + items.push(reactMarkdown(content)); + } + + if (function_call) { + const {name, arguments: args} = function_call; + items.push( + hbox({ + className: 'tb-msg__content--func', + items: [Icon.func(), div(`${name}(${args})`)] + }) + ); + } + + if (isEmpty(items)) { + items.push(errorMessage({error: 'No content returned - unexpected'})); + } + + return hbox({ + className: `tb-msg`, + items: [ + avatar({role}), + div({ + className: 'tb-msg__content', + items + }) + ] + }); + } +}); + +const avatar = hoistCmp.factory({ + render({role}) { + let item, + isIcon = true; + switch (role) { + case 'system': + item = Icon.gear(); + break; + case 'assistant': + item = Icon.icon({iconName: 'robot'}); + break; + case 'user': + item = img({src: XH.getUser().profilePicUrl, referrerPolicy: 'no-referrer'}); + isIcon = false; + break; + } + + return div({ + className: `tb-msg__avatar ${isIcon ? '' : 'tb-msg__avatar--icon'}`, + item + }); + } +}); + +const promptInput = hoistCmp.factory({ + model: uses(AppModel), + render({model}) { + const {inputMsg, taskObserver, inputRef} = model; + + return hbox({ + className: 'tb-prompt-input', + item: hbox({ + className: 'tb-prompt-input__inner', + items: [ + textArea({ + placeholder: 'Enter a message...', + flex: 1, + bind: 'inputMsg', + commitOnChange: true, + ref: inputRef, + disabled: taskObserver.isPending, + onKeyDown: e => model.onInputKeyDown(e) + }), + vbox({ + className: 'tb-prompt-input__buttons', + items: [ + button({ + icon: Icon.icon({iconName: 'paper-plane'}), + intent: 'success', + outlined: true, + tooltip: 'Send message - or press [Enter]', + disabled: !inputMsg || taskObserver.isPending, + onClick: () => model.submitAsync() + }), + vspacer(5), + button({ + icon: Icon.reset(), + intent: 'danger', + tooltip: 'Restart conversation', + disabled: isEmpty(XH.chatGptService.messages), + onClick: () => model.clearAndReInitAsync() + }), + vspacer(5), + popover({ + isOpen: model.showUserMessageHistory, + onClose: () => (model.showUserMessageHistory = false), + target: button({ + icon: Icon.history(), + intent: 'primary', + onClick: () => (model.showUserMessageHistory = true) + }), + content: userMsgHistory() + }) + ] + }) + ] + }) + }); + } +}); + +const appBarControls = hoistCmp.factory({ + render({model}) { + const popSize = {width: '70vw', minWidth: '800px', height: '80vh'}; + return fragment( + popover({ + target: button({ + text: 'Functions', + icon: Icon.func(), + outlined: true + }), + content: panel({ + title: 'Provided Function Library', + icon: Icon.func(), + compactHeader: true, + className: 'xh-popup--framed', + ...popSize, + item: jsonInput({ + value: JSON.stringify(XH.chatGptService.functions, null, 2), + readonly: true, + width: '100%', + height: '100%' + }) + }) + }), + appBarSeparator(), + popover({ + target: button({ + text: 'System Message', + icon: Icon.gear(), + outlined: true + }), + content: panel({ + title: 'Initial System Message', + icon: Icon.gear(), + compactHeader: true, + className: 'xh-popup--framed', + item: div({ + style: {...popSize, padding: 10, overflow: 'auto'}, + item: reactMarkdown({ + item: XH.chatGptService.systemMessage?.content ?? 'None found' + }) + }) + }) + }), + switchInput({ + value: XH.chatGptService.sendSystemMessage, + onChange: v => (XH.chatGptService.sendSystemMessage = v) + }), + appBarSeparator(), + modelSelector() + ); + } +}); + +const modelSelector = hoistCmp.factory({ + render() { + return select({ + enableFilter: false, + width: 150, + value: XH.chatGptService.model, + options: XH.chatGptService.selectableModels, + onChange: v => (XH.chatGptService.model = v) + }); + } +}); + +const userMsgHistory = hoistCmp.factory({ + render({model}) { + return panel({ + title: 'Message History', + icon: Icon.history(), + compactHeader: true, + headerItems: [ + button({ + text: 'Clear History', + icon: Icon.reset(), + onClick: () => XH.chatGptService.clearUserMessageHistory() + }) + ], + width: 600, + height: 300, + item: grid({model: model.userHistoryGridModel}) + }); + } +}); + +interface MsgItemProps extends HoistProps { + message: GptMessage; +} + +const reactMarkdown = elementFactory(ReactMarkdown); diff --git a/client-app/src/examples/chat/AppModel.ts b/client-app/src/examples/chat/AppModel.ts new file mode 100644 index 000000000..3aae81ac8 --- /dev/null +++ b/client-app/src/examples/chat/AppModel.ts @@ -0,0 +1,147 @@ +import {HoistAppModel, managed, TaskObserver, XH} from '@xh/hoist/core'; +import {OauthService} from '../../core/svc/OauthService'; +import {ChatGptService} from '../../core/svc/ChatGptService'; +import {bindable, when} from '@xh/hoist/mobx'; +import {wait} from '@xh/hoist/promise'; +import {createObservableRef} from '@xh/hoist/utils/react'; +import {HoistInputModel} from '@xh/hoist/cmp/input'; +import {isEmpty, last} from 'lodash'; +import {PortfolioService} from '../../core/svc/PortfolioService'; +import {GridModel} from '@xh/hoist/cmp/grid'; + +export class AppModel extends HoistAppModel { + static instance: AppModel; + + @bindable inputMsg: string; + @bindable showFunctionEditor: boolean = false; + + @bindable showUserMessageHistory = false; + @managed userHistoryGridModel: GridModel; + + taskObserver = TaskObserver.trackLast({message: 'Generating...'}); + + inputRef = createObservableRef(); + get input(): HoistInputModel { + return this.inputRef?.current as HoistInputModel; + } + + scrollRef = createObservableRef(); + + //------------------ + // Standard AppModel overrides + //------------------ + static override async preAuthAsync() { + await XH.installServicesAsync(OauthService); + } + + override async logoutAsync() { + await XH.oauthService.logoutAsync(); + } + + override get supportsVersionBar(): boolean { + return window.self === window.top; + } + + override async initAsync() { + await XH.installServicesAsync(ChatGptService, PortfolioService); + + this.userHistoryGridModel = this.createUsersHistoryGridModel(); + + this.addReaction( + { + track: () => [XH.chatGptService.messages, this.scrollRef.current], + run: () => this.scrollMessages() + }, + { + track: () => XH.pageIsActive, + run: isActive => { + if (isActive) this.focusInput(); + } + }, + { + track: () => XH.chatGptService.userMessageHistory, + run: msgs => { + this.userHistoryGridModel.loadData(msgs.map(message => ({message}))); + }, + fireImmediately: true + } + ); + + when( + () => !!this.input, + () => this.focusInput() + ); + } + + //------------------ + // Component logic + //------------------ + async submitAsync() { + const {inputMsg, taskObserver} = this; + if (!inputMsg) return; + + try { + await XH.chatGptService.sendChatAsync(inputMsg).linkTo(taskObserver); + // await wait(1000).linkTo(taskObserver); + this.inputMsg = ''; + this.focusInput(); + } catch (e) { + XH.handleException(e, {alertType: 'toast'}); + } + } + + onInputKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && !e.shiftKey) { + this.submitAsync(); + } else if (e.key === 'ArrowUp' && !this.inputMsg) { + const {userMessages} = XH.chatGptService; + if (!isEmpty(userMessages)) { + const lastMsg = last(userMessages).content; + this.inputMsg = lastMsg; + wait().then(() => { + this.input.inputEl.selectionStart = lastMsg.length; + this.input.inputEl.selectionEnd = lastMsg.length; + }); + } + } + } + + async clearAndReInitAsync() { + await XH.chatGptService.clearAndReInitAsync(); + XH.toast('Chat history cleared.'); + this.focusInput(); + } + + //------------------ + // Implementation + //------------------ + focusInput() { + wait(300).then(() => { + this.input?.focus(); + }); + } + + scrollMessages() { + wait(500).then(() => { + this.scrollRef.current?.scrollIntoView({behavior: 'auto'}); + }); + } + + createUsersHistoryGridModel() { + return new GridModel({ + store: { + idSpec: XH.genId + }, + emptyText: 'No messages yet...', + hideHeaders: true, + stripeRows: true, + rowBorders: true, + columns: [{field: 'message', flex: 1}], + onRowClicked: ({data: record}) => { + this.showUserMessageHistory = false; + this.inputMsg = record.data.message; + this.focusInput(); + } + }); + } +} diff --git a/client-app/src/examples/chat/Chat.scss b/client-app/src/examples/chat/Chat.scss new file mode 100644 index 000000000..8f8d0e2c5 --- /dev/null +++ b/client-app/src/examples/chat/Chat.scss @@ -0,0 +1,87 @@ +.tb-msg-list { + flex: 1; + overflow-y: scroll; +} + +.tb-msg { + padding: var(--xh-pad-px); + border-bottom: 1px solid var(--xh-grid-border-color); + + &:nth-child(odd) { + background-color: var(--xh-grid-bg-odd); + } + + &__avatar { + color: var(--xh-orange-muted); + width: 40px; + margin-right: var(--xh-pad-double-px); + + > * { + border-radius: 10px; + } + + img { + width: 40px; + height: 40px; + } + + .xh-icon { + width: 30px; + height: 30px; + padding: 4px; + border: 1px solid var(--xh-orange-muted); + } + } + + &__content { + &--func { + align-items: center; + margin: 4px; + border: var(--xh-border-solid); + border-radius: var(--xh-border-radius-px); + font-family: var(--xh-font-family-mono); + font-size: 0.8em; + + .xh-icon { + padding: var(--xh-pad-px); + color: var(--xh-orange-muted); + font-size: 1.5em; + border-right: var(--xh-border-solid); + } + + > div { + padding: 10px; + } + } + + pre { + padding: var(--xh-pad-px); + border: var(--xh-border-solid); + border-radius: var(--xh-border-radius-px); + font-family: var(--xh-font-family-mono); + font-size: 0.8em; + } + } +} + +.tb-prompt-input { + flex: none; + padding: 10px; + justify-content: center; + border-top: 2px solid var(--xh-border-color); + background-color: var(--xh-bg-alt); + + &__inner { + width: 80vw; + align-items: stretch; + + textarea { + border-radius: 8px; + } + } + + &__buttons { + margin-left: 10px; + justify-content: center; + } +} diff --git a/client-app/src/examples/chat/system-message.md b/client-app/src/examples/chat/system-message.md new file mode 100644 index 000000000..bf301c9fd --- /dev/null +++ b/client-app/src/examples/chat/system-message.md @@ -0,0 +1,43 @@ +You are a professional AI assistant embedded within a custom financial reporting dashboard application created by a +hedge fund with headquarters in the United States. Your role is to respond to user queries with either a function call +that the application can run OR a message asking the user to clarify or explaining why you are unable to help. + +### Objects returned and aggregated by the application API + +The `getPortfolioPositions` function returns a list of `Position` objects. A `Position` satisfies the following +interface: + +```typescript +interface Position { + id: string; + name: string; + pnl: number; + mktVal: number; + children: Position[]; +} +``` + +A `Position` represents an aggregate of one or more `RawPosition` objects. A `RawPosition` models a single investment +within a portfolio. It satisfies the following interface: + +```typescript +interface RawPosition { + // Dimension - the stock ticker or identifier of the position's instrument, an equity stock or other security - e.g. ['AAPL', 'GOOG', 'MSFT'] + symbol: string; + // Dimension - the industry sector of the instrument - e.g. ['Technology', 'Healthcare', 'Energy'] + sector: string; + // Dimension - the name of an investment fund - e.g. ['Winter Star Fund', 'Oak Mount Fund'] + fund: string; + // Dimension - the name of the trader or portfolio manager responsible for the investment - e.g. ['Susan Major', 'Fred Corn', 'HedgeSys'] + trader: string; + // Measure - the current value of the position, in USD. + mktVal: number; + // Measure - the current profit and loss of the position, in USD. + pnl: number; +} +``` + +The `getPortfolioPositions` function takes a list of `groupByDimensions` when aggregating results, representing +the field names of `RawPosition` dimensions within the portfolio data. + +Introduce yourself to the user and ask them how you can help them. diff --git a/client-app/yarn.lock b/client-app/yarn.lock index e1c541216..ad32941a1 100644 --- a/client-app/yarn.lock +++ b/client-app/yarn.lock @@ -1597,6 +1597,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.0.0": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + dependencies: + "@types/ms" "*" + "@types/dom4@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.2.tgz#6495303f049689ce936ed328a3e5ede9c51408ee" @@ -1651,6 +1658,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/hast@^2.0.0": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.5.tgz#08caac88b44d0fdd04dc17a19142355f43bd8a7a" + integrity sha512-SvQi0L/lNpThgPoleH53cdjB3y9zpLlVjRbqB3rH8hx1jiRSBGAhyjV3H+URFjNVRqt2EdYNrbZE5IsGlNfpRg== + dependencies: + "@types/unist" "^2" + "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -1686,6 +1700,13 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== +"@types/mdast@^3.0.0": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514" + integrity sha512-DT+iNIRNX884cx0/Q1ja7NyUPpZuv0KPyL5rGNxm1WC1OtHstl7n4Jb7nk+xacNShQMbczJjt8uFzznpp6kYBg== + dependencies: + "@types/unist" "^2" + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -1706,6 +1727,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + "@types/node@*": version "20.4.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" @@ -1721,7 +1747,7 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/prop-types@*": +"@types/prop-types@*", "@types/prop-types@^15.0.0": version "15.7.5" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== @@ -1848,6 +1874,11 @@ dependencies: "@types/node" "*" +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.7.tgz#5b06ad6894b236a1d2bd6b2f07850ca5c59cf4d6" + integrity sha512-cputDpIbFgLUaGQn6Vqg3/YsJwxUwHLO13v3i5ouxT4lat0khip9AEWxtERujXV9wxIB1EyF97BSJFt6vpdI8g== + "@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" @@ -2538,6 +2569,11 @@ babel-plugin-transform-imports@~2.0.0: "@babel/types" "^7.4" is-valid-path "^0.1.1" +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2763,6 +2799,11 @@ changelog-parser@~3.0.1: line-reader "^0.2.4" remove-markdown "^0.5.0" +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -2888,6 +2929,11 @@ colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.19: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@^10.0.0, commander@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" @@ -3127,7 +3173,7 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3152,6 +3198,13 @@ decamelize@^5.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -3217,6 +3270,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -3227,6 +3285,11 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== +diff@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3738,6 +3801,11 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4190,6 +4258,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hast-util-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" + integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== + he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -4437,6 +4510,11 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inter-ui@~3.19.3: version "3.19.3" resolved "https://registry.yarnpkg.com/inter-ui/-/inter-ui-3.19.3.tgz#cf4b4b6d30de8d5463e2462588654b325206488c" @@ -4510,6 +4588,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -4626,6 +4709,11 @@ is-plain-obj@^3.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -4811,6 +4899,11 @@ kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +kleur@^4.0.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + known-css-properties@^0.27.0: version "0.27.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" @@ -5008,6 +5101,54 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== +mdast-util-definitions@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" + integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-to-hast@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" + integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-definitions "^5.0.0" + micromark-util-sanitize-uri "^1.1.0" + trim-lines "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + +mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdn-data@2.0.30: version "2.0.30" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" @@ -5068,6 +5209,200 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" @@ -5168,6 +5503,11 @@ moment@2.x, moment@>=1.6.0, moment@~2.29.1: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + mrmime@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" @@ -5789,7 +6129,7 @@ prop-types-exact@^1.2.0: object.assign "^4.1.0" reflect.ownkeys "^0.2.0" -prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.x, prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -5798,6 +6138,11 @@ prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, pro object-assign "^4.1.1" react-is "^16.13.1" +property-information@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" + integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -5968,6 +6313,32 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-markdown@~8.0.7: + version "8.0.7" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b" + integrity sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ== + dependencies: + "@types/hast" "^2.0.0" + "@types/prop-types" "^15.0.0" + "@types/unist" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^2.0.0" + prop-types "^15.0.0" + property-information "^6.0.0" + react-is "^18.0.0" + remark-parse "^10.0.0" + remark-rehype "^10.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + react-moment-proptypes@^1.6.0: version "1.8.1" resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz#7ba4076147f6b5998f0d4f51d302d6d8c62049fd" @@ -6256,6 +6627,25 @@ relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== +remark-parse@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-rehype@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + remove-markdown@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.5.0.tgz#a596264bbd60b9ceab2e2ae86e5789eee91aee32" @@ -6407,6 +6797,13 @@ rxjs@^7.8.0: dependencies: tslib "^2.1.0" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-array-concat@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" @@ -6713,6 +7110,11 @@ source-map@^0.6.0, source-map@~0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -6906,6 +7308,13 @@ style-search@^0.1.0: resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== +style-to-object@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37" + integrity sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw== + dependencies: + inline-style-parser "0.1.1" + stylelint-config-recommended-scss@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-12.0.0.tgz#9d9e82c46012649f11bfebcbc788f58e61860f33" @@ -7119,11 +7528,21 @@ totalist@^1.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + trim-newlines@^4.0.2: version "4.1.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== +trough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" + integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== + ts-api-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" @@ -7261,6 +7680,62 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unified@^10.0.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + +unist-util-generated@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" + integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-position@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" + integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -7329,6 +7804,16 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -7342,6 +7827,24 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile@^5.0.0: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + warning@^4.0.2, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" diff --git a/grails-app/controllers/io/xh/toolbox/admin/ChatGptController.groovy b/grails-app/controllers/io/xh/toolbox/admin/ChatGptController.groovy new file mode 100644 index 000000000..a07aa7caa --- /dev/null +++ b/grails-app/controllers/io/xh/toolbox/admin/ChatGptController.groovy @@ -0,0 +1,23 @@ +package io.xh.toolbox.admin + + +import io.xh.hoist.security.Access +import io.xh.toolbox.BaseController + +@Access('CHAT_GPT_USER') +class ChatGptController extends BaseController { + + def configService + + def config() { + // TODO - would like to get initialSystemMessage from config, but config editor doesn't persist newlines + // which then breaks markdown formatting/detection. Prob. not important to GPT but doesn't look as + // good in the UI. Currently copied into ChatGptService.ts. +// renderJSON([ +// *:configService.getMap('chatGptConfig'), +// initialSystemMessage: configService.getString('chatGptInitialSystemMessage') +// ]) + renderJSON(configService.getMap('chatGptConfig')) + } + +} diff --git a/src/main/groovy/io/xh/toolbox/portfolio/RawPosition.groovy b/src/main/groovy/io/xh/toolbox/portfolio/RawPosition.groovy index eab04d461..b2afc4297 100644 --- a/src/main/groovy/io/xh/toolbox/portfolio/RawPosition.groovy +++ b/src/main/groovy/io/xh/toolbox/portfolio/RawPosition.groovy @@ -51,6 +51,7 @@ class RawPosition extends JSONFormatCached { return [ symbol: symbol, model : model, + sector: sector, fund : fund, trader: trader, mktVal: mktVal, From 74ea05e1385600c65e7ac2cf08010c554d934c4c Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 27 Jul 2023 07:53:03 -0400 Subject: [PATCH 2/8] Interim component refactor / break out of AppModel --- client-app/src/examples/chat/AppComponent.ts | 187 +------------- client-app/src/examples/chat/AppModel.ts | 125 +--------- client-app/src/examples/chat/ChatModel.ts | 125 ++++++++++ client-app/src/examples/chat/ChatPanel.ts | 28 +++ .../src/examples/chat/cmp/MessageList.ts | 102 ++++++++ .../src/examples/chat/cmp/PromptInput.ts | 87 +++++++ client-app/yarn.lock | 235 +++++++++--------- 7 files changed, 463 insertions(+), 426 deletions(-) create mode 100644 client-app/src/examples/chat/ChatModel.ts create mode 100644 client-app/src/examples/chat/ChatPanel.ts create mode 100644 client-app/src/examples/chat/cmp/MessageList.ts create mode 100644 client-app/src/examples/chat/cmp/PromptInput.ts diff --git a/client-app/src/examples/chat/AppComponent.ts b/client-app/src/examples/chat/AppComponent.ts index de5a38aee..9583f0135 100644 --- a/client-app/src/examples/chat/AppComponent.ts +++ b/client-app/src/examples/chat/AppComponent.ts @@ -1,21 +1,18 @@ import {library} from '@fortawesome/fontawesome-svg-core'; import {faPaperPlane, faRobot, faUserRobotXmarks} from '@fortawesome/pro-regular-svg-icons'; -import {elementFactory, hoistCmp, HoistProps, uses, XH} from '@xh/hoist/core'; +import {elementFactory, hoistCmp, uses, XH} from '@xh/hoist/core'; import {appBar, appBarSeparator} from '@xh/hoist/desktop/cmp/appbar'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {Icon} from '@xh/hoist/icon'; import {AppModel} from './AppModel'; import '../../core/Toolbox.scss'; -import {box, div, fragment, hbox, img, placeholder, vbox, vspacer} from '@xh/hoist/cmp/layout'; -import {jsonInput, select, switchInput, textArea} from '@xh/hoist/desktop/cmp/input'; +import {div, fragment} from '@xh/hoist/cmp/layout'; +import {jsonInput, select, switchInput} from '@xh/hoist/desktop/cmp/input'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {isEmpty} from 'lodash'; -import {GptMessage} from '../../core/svc/ChatGptService'; import ReactMarkdown from 'react-markdown'; import './Chat.scss'; -import {errorMessage} from '@xh/hoist/desktop/cmp/error'; import {popover} from '@xh/hoist/kit/blueprint'; -import {grid} from '@xh/hoist/cmp/grid'; +import {chatPanel} from './ChatPanel'; library.add(faPaperPlane, faRobot, faUserRobotXmarks); @@ -30,157 +27,7 @@ export const AppComponent = hoistCmp({ appMenuButtonProps: {hideLogoutItem: false}, rightItems: [appBarControls()] }), - items: XH.chatGptService.isInitialized - ? [msgList(), promptInput()] - : placeholder( - Icon.icon({iconName: 'user-robot-xmarks'}), - 'ChatGPTService not initialized.', - button({ - text: 'Retry', - onClick: () => XH.chatGptService.clearAndReInitAsync() - }) - ) - }); - } -}); - -const msgList = hoistCmp.factory({ - render({model}) { - const {messages} = XH.chatGptService, - item = isEmpty(messages) - ? placeholder(Icon.ellipsisHorizontal(), 'No messages yet...') - : div({ - className: 'tb-msg-list', - items: [ - ...messages.map(message => msgItem({message})), - // Supports scrolling to bottom of list. - box({ref: model.scrollRef}) - ] - }); - - return panel({ - item, - loadingIndicator: model.taskObserver - }); - } -}); - -const msgItem = hoistCmp.factory({ - render({model, message}) { - const {role, content, function_call} = message, - items = []; - - // System message is visible via popover from top toolbar. - if (role === 'system') return null; - - if (content) { - items.push(reactMarkdown(content)); - } - - if (function_call) { - const {name, arguments: args} = function_call; - items.push( - hbox({ - className: 'tb-msg__content--func', - items: [Icon.func(), div(`${name}(${args})`)] - }) - ); - } - - if (isEmpty(items)) { - items.push(errorMessage({error: 'No content returned - unexpected'})); - } - - return hbox({ - className: `tb-msg`, - items: [ - avatar({role}), - div({ - className: 'tb-msg__content', - items - }) - ] - }); - } -}); - -const avatar = hoistCmp.factory({ - render({role}) { - let item, - isIcon = true; - switch (role) { - case 'system': - item = Icon.gear(); - break; - case 'assistant': - item = Icon.icon({iconName: 'robot'}); - break; - case 'user': - item = img({src: XH.getUser().profilePicUrl, referrerPolicy: 'no-referrer'}); - isIcon = false; - break; - } - - return div({ - className: `tb-msg__avatar ${isIcon ? '' : 'tb-msg__avatar--icon'}`, - item - }); - } -}); - -const promptInput = hoistCmp.factory({ - model: uses(AppModel), - render({model}) { - const {inputMsg, taskObserver, inputRef} = model; - - return hbox({ - className: 'tb-prompt-input', - item: hbox({ - className: 'tb-prompt-input__inner', - items: [ - textArea({ - placeholder: 'Enter a message...', - flex: 1, - bind: 'inputMsg', - commitOnChange: true, - ref: inputRef, - disabled: taskObserver.isPending, - onKeyDown: e => model.onInputKeyDown(e) - }), - vbox({ - className: 'tb-prompt-input__buttons', - items: [ - button({ - icon: Icon.icon({iconName: 'paper-plane'}), - intent: 'success', - outlined: true, - tooltip: 'Send message - or press [Enter]', - disabled: !inputMsg || taskObserver.isPending, - onClick: () => model.submitAsync() - }), - vspacer(5), - button({ - icon: Icon.reset(), - intent: 'danger', - tooltip: 'Restart conversation', - disabled: isEmpty(XH.chatGptService.messages), - onClick: () => model.clearAndReInitAsync() - }), - vspacer(5), - popover({ - isOpen: model.showUserMessageHistory, - onClose: () => (model.showUserMessageHistory = false), - target: button({ - icon: Icon.history(), - intent: 'primary', - onClick: () => (model.showUserMessageHistory = true) - }), - content: userMsgHistory() - }) - ] - }) - ] - }) + items: chatPanel() }); } }); @@ -251,28 +98,4 @@ const modelSelector = hoistCmp.factory({ } }); -const userMsgHistory = hoistCmp.factory({ - render({model}) { - return panel({ - title: 'Message History', - icon: Icon.history(), - compactHeader: true, - headerItems: [ - button({ - text: 'Clear History', - icon: Icon.reset(), - onClick: () => XH.chatGptService.clearUserMessageHistory() - }) - ], - width: 600, - height: 300, - item: grid({model: model.userHistoryGridModel}) - }); - } -}); - -interface MsgItemProps extends HoistProps { - message: GptMessage; -} - const reactMarkdown = elementFactory(ReactMarkdown); diff --git a/client-app/src/examples/chat/AppModel.ts b/client-app/src/examples/chat/AppModel.ts index 3aae81ac8..1800beb5f 100644 --- a/client-app/src/examples/chat/AppModel.ts +++ b/client-app/src/examples/chat/AppModel.ts @@ -1,35 +1,11 @@ -import {HoistAppModel, managed, TaskObserver, XH} from '@xh/hoist/core'; +import {HoistAppModel, XH} from '@xh/hoist/core'; import {OauthService} from '../../core/svc/OauthService'; import {ChatGptService} from '../../core/svc/ChatGptService'; -import {bindable, when} from '@xh/hoist/mobx'; -import {wait} from '@xh/hoist/promise'; -import {createObservableRef} from '@xh/hoist/utils/react'; -import {HoistInputModel} from '@xh/hoist/cmp/input'; -import {isEmpty, last} from 'lodash'; import {PortfolioService} from '../../core/svc/PortfolioService'; -import {GridModel} from '@xh/hoist/cmp/grid'; export class AppModel extends HoistAppModel { static instance: AppModel; - @bindable inputMsg: string; - @bindable showFunctionEditor: boolean = false; - - @bindable showUserMessageHistory = false; - @managed userHistoryGridModel: GridModel; - - taskObserver = TaskObserver.trackLast({message: 'Generating...'}); - - inputRef = createObservableRef(); - get input(): HoistInputModel { - return this.inputRef?.current as HoistInputModel; - } - - scrollRef = createObservableRef(); - - //------------------ - // Standard AppModel overrides - //------------------ static override async preAuthAsync() { await XH.installServicesAsync(OauthService); } @@ -44,104 +20,5 @@ export class AppModel extends HoistAppModel { override async initAsync() { await XH.installServicesAsync(ChatGptService, PortfolioService); - - this.userHistoryGridModel = this.createUsersHistoryGridModel(); - - this.addReaction( - { - track: () => [XH.chatGptService.messages, this.scrollRef.current], - run: () => this.scrollMessages() - }, - { - track: () => XH.pageIsActive, - run: isActive => { - if (isActive) this.focusInput(); - } - }, - { - track: () => XH.chatGptService.userMessageHistory, - run: msgs => { - this.userHistoryGridModel.loadData(msgs.map(message => ({message}))); - }, - fireImmediately: true - } - ); - - when( - () => !!this.input, - () => this.focusInput() - ); - } - - //------------------ - // Component logic - //------------------ - async submitAsync() { - const {inputMsg, taskObserver} = this; - if (!inputMsg) return; - - try { - await XH.chatGptService.sendChatAsync(inputMsg).linkTo(taskObserver); - // await wait(1000).linkTo(taskObserver); - this.inputMsg = ''; - this.focusInput(); - } catch (e) { - XH.handleException(e, {alertType: 'toast'}); - } - } - - onInputKeyDown(e: KeyboardEvent) { - if (e.key === 'Enter' && !e.shiftKey) { - this.submitAsync(); - } else if (e.key === 'ArrowUp' && !this.inputMsg) { - const {userMessages} = XH.chatGptService; - if (!isEmpty(userMessages)) { - const lastMsg = last(userMessages).content; - this.inputMsg = lastMsg; - wait().then(() => { - this.input.inputEl.selectionStart = lastMsg.length; - this.input.inputEl.selectionEnd = lastMsg.length; - }); - } - } - } - - async clearAndReInitAsync() { - await XH.chatGptService.clearAndReInitAsync(); - XH.toast('Chat history cleared.'); - this.focusInput(); - } - - //------------------ - // Implementation - //------------------ - focusInput() { - wait(300).then(() => { - this.input?.focus(); - }); - } - - scrollMessages() { - wait(500).then(() => { - this.scrollRef.current?.scrollIntoView({behavior: 'auto'}); - }); - } - - createUsersHistoryGridModel() { - return new GridModel({ - store: { - idSpec: XH.genId - }, - emptyText: 'No messages yet...', - hideHeaders: true, - stripeRows: true, - rowBorders: true, - columns: [{field: 'message', flex: 1}], - onRowClicked: ({data: record}) => { - this.showUserMessageHistory = false; - this.inputMsg = record.data.message; - this.focusInput(); - } - }); } } diff --git a/client-app/src/examples/chat/ChatModel.ts b/client-app/src/examples/chat/ChatModel.ts new file mode 100644 index 000000000..a5582fdbc --- /dev/null +++ b/client-app/src/examples/chat/ChatModel.ts @@ -0,0 +1,125 @@ +import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; +import {bindable, when} from '@xh/hoist/mobx'; +import {GridModel} from '@xh/hoist/cmp/grid'; +import {createObservableRef} from '@xh/hoist/utils/react'; +import {HoistInputModel} from '@xh/hoist/cmp/input'; +import {isEmpty, last} from 'lodash'; +import {wait} from '@xh/hoist/promise'; + +export class ChatModel extends HoistModel { + @bindable inputMsg: string; + @bindable showFunctionEditor: boolean = false; + + @bindable showUserMessageHistory = false; + @managed userHistoryGridModel: GridModel; + + taskObserver = TaskObserver.trackLast({message: 'Generating...'}); + + inputRef = createObservableRef(); + get input(): HoistInputModel { + return this.inputRef?.current as HoistInputModel; + } + + scrollRef = createObservableRef(); + + async initAsync() { + this.userHistoryGridModel = this.createUsersHistoryGridModel(); + + this.addReaction( + { + track: () => [XH.chatGptService.messages, this.scrollRef.current], + run: () => this.scrollMessages() + }, + { + track: () => XH.pageIsActive, + run: isActive => { + if (isActive) this.focusInput(); + } + }, + { + track: () => XH.chatGptService.userMessageHistory, + run: msgs => { + this.userHistoryGridModel.loadData(msgs.map(message => ({message}))); + }, + fireImmediately: true + } + ); + + when( + () => !!this.input, + () => this.focusInput() + ); + } + + //------------------ + // Component logic + //------------------ + async submitAsync() { + const {inputMsg, taskObserver} = this; + if (!inputMsg) return; + + try { + await XH.chatGptService.sendChatAsync(inputMsg).linkTo(taskObserver); + // await wait(1000).linkTo(taskObserver); + this.inputMsg = ''; + this.focusInput(); + } catch (e) { + XH.handleException(e, {alertType: 'toast'}); + } + } + + onInputKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && !e.shiftKey) { + this.submitAsync(); + } else if (e.key === 'ArrowUp' && !this.inputMsg) { + const {userMessages} = XH.chatGptService; + if (!isEmpty(userMessages)) { + const lastMsg = last(userMessages).content; + this.inputMsg = lastMsg; + wait().then(() => { + this.input.inputEl.selectionStart = lastMsg.length; + this.input.inputEl.selectionEnd = lastMsg.length; + }); + } + } + } + + async clearAndReInitAsync() { + await XH.chatGptService.clearAndReInitAsync(); + XH.toast('Chat history cleared.'); + this.focusInput(); + } + + //------------------ + // Implementation + //------------------ + focusInput() { + wait(300).then(() => { + this.input?.focus(); + }); + } + + scrollMessages() { + wait(500).then(() => { + this.scrollRef.current?.scrollIntoView({behavior: 'auto'}); + }); + } + + createUsersHistoryGridModel() { + return new GridModel({ + store: { + idSpec: XH.genId + }, + emptyText: 'No messages yet...', + hideHeaders: true, + stripeRows: true, + rowBorders: true, + columns: [{field: 'message', flex: 1}], + onRowClicked: ({data: record}) => { + this.showUserMessageHistory = false; + this.inputMsg = record.data.message; + this.focusInput(); + } + }); + } +} diff --git a/client-app/src/examples/chat/ChatPanel.ts b/client-app/src/examples/chat/ChatPanel.ts new file mode 100644 index 000000000..1f74c7137 --- /dev/null +++ b/client-app/src/examples/chat/ChatPanel.ts @@ -0,0 +1,28 @@ +import {creates, hoistCmp, XH} from '@xh/hoist/core'; +import {ChatModel} from './ChatModel'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {messageList} from './cmp/MessageList'; +import {promptInput} from './cmp/PromptInput'; +import {placeholder} from '@xh/hoist/cmp/layout'; +import {Icon} from '@xh/hoist/icon'; +import {button} from '@xh/hoist/desktop/cmp/button'; + +export const chatPanel = hoistCmp.factory({ + displayName: 'ChatPanel', + model: creates(ChatModel), + + render({model}) { + return panel({ + items: XH.chatGptService.isInitialized + ? [messageList(), promptInput()] + : placeholder( + Icon.icon({iconName: 'user-robot-xmarks'}), + 'ChatGPTService not initialized.', + button({ + text: 'Retry', + onClick: () => XH.chatGptService.clearAndReInitAsync() + }) + ) + }); + } +}); diff --git a/client-app/src/examples/chat/cmp/MessageList.ts b/client-app/src/examples/chat/cmp/MessageList.ts new file mode 100644 index 000000000..c6eb9528e --- /dev/null +++ b/client-app/src/examples/chat/cmp/MessageList.ts @@ -0,0 +1,102 @@ +import {elementFactory, hoistCmp, HoistProps, uses, XH} from '@xh/hoist/core'; +import {isEmpty} from 'lodash'; +import {box, div, hbox, img, placeholder} from '@xh/hoist/cmp/layout'; +import {Icon} from '@xh/hoist/icon'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {errorMessage} from '@xh/hoist/desktop/cmp/error'; +import {GptMessage} from '../../../core/svc/ChatGptService'; +import ReactMarkdown from 'react-markdown'; +import {ChatModel} from '../ChatModel'; + +export const messageList = hoistCmp.factory({ + displayName: 'MessageList', + model: uses(ChatModel), + + render({model}) { + const {messages} = XH.chatGptService, + item = isEmpty(messages) + ? placeholder(Icon.ellipsisHorizontal(), 'No messages yet...') + : div({ + className: 'tb-msg-list', + items: [ + ...messages.map(message => msgItem({message})), + // Supports scrolling to bottom of list. + box({ref: model.scrollRef}) + ] + }); + + return panel({ + item, + loadingIndicator: model.taskObserver + }); + } +}); + +const msgItem = hoistCmp.factory({ + render({model, message}) { + const {role, content, function_call} = message, + items = []; + + // System message is visible via popover from top toolbar. + if (role === 'system') return null; + + if (content) { + items.push(reactMarkdown(content)); + } + + if (function_call) { + const {name, arguments: args} = function_call; + items.push( + hbox({ + className: 'tb-msg__content--func', + items: [Icon.func(), div(`${name}(${args})`)] + }) + ); + } + + if (isEmpty(items)) { + items.push(errorMessage({error: 'No content returned - unexpected'})); + } + + return hbox({ + className: `tb-msg`, + items: [ + avatar({role}), + div({ + className: 'tb-msg__content', + items + }) + ] + }); + } +}); + +const avatar = hoistCmp.factory({ + render({role}) { + let item, + isIcon = true; + switch (role) { + case 'system': + item = Icon.gear(); + break; + case 'assistant': + item = Icon.icon({iconName: 'robot'}); + break; + case 'user': + item = img({src: XH.getUser().profilePicUrl, referrerPolicy: 'no-referrer'}); + isIcon = false; + break; + } + + return div({ + className: `tb-msg__avatar ${isIcon ? '' : 'tb-msg__avatar--icon'}`, + item + }); + } +}); + +const reactMarkdown = elementFactory(ReactMarkdown); + +interface MsgItemProps extends HoistProps { + message: GptMessage; +} diff --git a/client-app/src/examples/chat/cmp/PromptInput.ts b/client-app/src/examples/chat/cmp/PromptInput.ts new file mode 100644 index 000000000..280ce263a --- /dev/null +++ b/client-app/src/examples/chat/cmp/PromptInput.ts @@ -0,0 +1,87 @@ +import {hoistCmp, uses, XH} from '@xh/hoist/core'; +import {hbox, vbox, vspacer} from '@xh/hoist/cmp/layout'; +import {textArea} from '@xh/hoist/desktop/cmp/input'; +import {button} from '@xh/hoist/desktop/cmp/button'; +import {Icon} from '@xh/hoist/icon'; +import {isEmpty} from 'lodash'; +import {popover} from '@xh/hoist/kit/blueprint'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {grid} from '@xh/hoist/cmp/grid'; +import {ChatModel} from '../ChatModel'; + +export const promptInput = hoistCmp.factory({ + model: uses(ChatModel), + render({model}) { + const {inputMsg, taskObserver, inputRef} = model; + + return hbox({ + className: 'tb-prompt-input', + item: hbox({ + className: 'tb-prompt-input__inner', + items: [ + textArea({ + placeholder: 'Enter a message...', + flex: 1, + bind: 'inputMsg', + commitOnChange: true, + ref: inputRef, + disabled: taskObserver.isPending, + onKeyDown: e => model.onInputKeyDown(e) + }), + vbox({ + className: 'tb-prompt-input__buttons', + items: [ + button({ + icon: Icon.icon({iconName: 'paper-plane'}), + intent: 'success', + outlined: true, + tooltip: 'Send message - or press [Enter]', + disabled: !inputMsg || taskObserver.isPending, + onClick: () => model.submitAsync() + }), + vspacer(5), + button({ + icon: Icon.reset(), + intent: 'danger', + tooltip: 'Restart conversation', + disabled: isEmpty(XH.chatGptService.messages), + onClick: () => model.clearAndReInitAsync() + }), + vspacer(5), + popover({ + isOpen: model.showUserMessageHistory, + onClose: () => (model.showUserMessageHistory = false), + target: button({ + icon: Icon.history(), + intent: 'primary', + onClick: () => (model.showUserMessageHistory = true) + }), + content: userMsgHistory() + }) + ] + }) + ] + }) + }); + } +}); + +const userMsgHistory = hoistCmp.factory({ + render({model}) { + return panel({ + title: 'Message History', + icon: Icon.history(), + compactHeader: true, + headerItems: [ + button({ + text: 'Clear History', + icon: Icon.reset(), + onClick: () => XH.chatGptService.clearUserMessageHistory() + }) + ], + width: 600, + height: 300, + item: grid({model: model.userHistoryGridModel}) + }); + } +}); diff --git a/client-app/yarn.lock b/client-app/yarn.lock index ad32941a1..89f14b023 100644 --- a/client-app/yarn.lock +++ b/client-app/yarn.lock @@ -236,10 +236,10 @@ regexpu-core "^5.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.1.tgz#af1429c4a83ac316a6a8c2cc8ff45cb5d2998d3a" - integrity sha512-kX4oXixDxG197yhX+J3Wp+NpL2wuCFjWQAr6yX2jtCnflK9ulMI51ULFGIrWiX1jGfvAxdHp+XQCcP2bZGPs9A== +"@babel/helper-define-polyfill-provider@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz#82c825cadeeeee7aad237618ebbe8fa1710015d7" + integrity sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw== dependencies: "@babel/helper-compilation-targets" "^7.22.6" "@babel/helper-plugin-utils" "^7.22.5" @@ -1096,9 +1096,9 @@ semver "^6.3.1" "@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + version "0.1.6" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6.tgz#31bcdd8f19538437339d17af00d177d854d9d458" + integrity sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -1229,19 +1229,19 @@ semver "^7.3.4" "@csstools/css-parser-algorithms@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" - integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" + integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== "@csstools/css-tokenizer@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" - integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" + integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== "@csstools/media-query-list-parser@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" - integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.3.tgz#4471ebd436a22019378fe9c8ac8c0f30c4fbb796" + integrity sha512-ATul1u+pic4aVpstgueqxEv4MsObEbszAxfTXpx9LHaeD3LAh+wFqdCteyegWmjk0k5rkSCAvIOaJe9U3DD09w== "@csstools/selector-specificity@^3.0.0": version "3.0.0" @@ -1349,9 +1349,9 @@ eslint-visitor-keys "^3.3.0" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.1": - version "4.5.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" - integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== "@eslint/eslintrc@^2.1.0": version "2.1.0" @@ -1521,11 +1521,6 @@ dependencies: eslint-scope "5.1.1" -"@nicolo-ribaudo/semver-v6@^6.3.3": - version "6.3.3" - resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz#ea6d23ade78a325f7a52750aab1526b02b628c29" - integrity sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1618,9 +1613,9 @@ "@types/estree" "*" "@types/eslint@*": - version "8.44.0" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.0.tgz#55818eabb376e2272f77fbf5c96c43137c3c1e53" - integrity sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw== + version "8.44.1" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.1.tgz#d1811559bb6bcd1a76009e3f7883034b78a0415e" + integrity sha512-XpNDc4Z5Tb4x+SW1MriMVeIsMoONHCkWFMkR/aPJbzEsxqHy+4Glu/BqTdPrApfDeMaXbtNh6bseNgl5KaWrSg== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -1696,9 +1691,9 @@ integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== "@types/lodash@4.x": - version "4.14.195" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.195.tgz#bafc975b252eb6cea78882ce8a7b6bf22a6de632" - integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg== + version "4.14.196" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.196.tgz#a7c3d6fc52d8d71328b764e28e080b4169ec7a95" + integrity sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ== "@types/mdast@^3.0.0": version "3.0.12" @@ -1733,9 +1728,9 @@ integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== "@types/node@*": - version "20.4.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9" - integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw== + version "20.4.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69" + integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -1811,9 +1806,9 @@ "@types/react" "*" "@types/react@*", "@types/react@18.x": - version "18.2.15" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.15.tgz#14792b35df676c20ec3cf595b262f8c615a73066" - integrity sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA== + version "18.2.16" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.16.tgz#403dda0e933caccac9efde569923239ac426786c" + integrity sha512-LLFWr12ZhBJ4YVw7neWLe6Pk7Ey5R9OCydfuMsz1L8bZxzaawJj2p06Q8/EFEHDeTBQNFLF62X+CG7B2zIyu0Q== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -1887,15 +1882,15 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin@^6.1": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.1.0.tgz#96f3ca6615717659d06c9f7161a1d14ab0c49c66" - integrity sha512-qg7Bm5TyP/I7iilGyp6DRqqkt8na00lI6HbjWZObgk3FFSzH5ypRwAHXJhJkwiRtTcfn+xYQIMOR5kJgpo6upw== + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.2.0.tgz#57047c400be0632d4797ac081af8d399db3ebc3b" + integrity sha512-rClGrMuyS/3j0ETa1Ui7s6GkLhfZGKZL3ZrChLeAiACBE/tRc1wq8SNZESUuluxhLj9FkUefRs2l6bCIArWBiQ== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.1.0" - "@typescript-eslint/type-utils" "6.1.0" - "@typescript-eslint/utils" "6.1.0" - "@typescript-eslint/visitor-keys" "6.1.0" + "@typescript-eslint/scope-manager" "6.2.0" + "@typescript-eslint/type-utils" "6.2.0" + "@typescript-eslint/utils" "6.2.0" + "@typescript-eslint/visitor-keys" "6.2.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -1905,71 +1900,71 @@ ts-api-utils "^1.0.1" "@typescript-eslint/parser@^6.1": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.1.0.tgz#3135bf65dca5340d8650703eb8cb83113e156ee5" - integrity sha512-hIzCPvX4vDs4qL07SYzyomamcs2/tQYXg5DtdAfj35AyJ5PIUqhsLf4YrEIFzZcND7R2E8tpQIZKayxg8/6Wbw== + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.2.0.tgz#d37c30b0f459c6f39455335d8f4f085919a1c644" + integrity sha512-igVYOqtiK/UsvKAmmloQAruAdUHihsOCvplJpplPZ+3h4aDkC/UKZZNKgB6h93ayuYLuEymU3h8nF1xMRbh37g== dependencies: - "@typescript-eslint/scope-manager" "6.1.0" - "@typescript-eslint/types" "6.1.0" - "@typescript-eslint/typescript-estree" "6.1.0" - "@typescript-eslint/visitor-keys" "6.1.0" + "@typescript-eslint/scope-manager" "6.2.0" + "@typescript-eslint/types" "6.2.0" + "@typescript-eslint/typescript-estree" "6.2.0" + "@typescript-eslint/visitor-keys" "6.2.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.1.0.tgz#a6cdbe11630614f8c04867858a42dd56590796ed" - integrity sha512-AxjgxDn27hgPpe2rQe19k0tXw84YCOsjDJ2r61cIebq1t+AIxbgiXKvD4999Wk49GVaAcdJ/d49FYel+Pp3jjw== +"@typescript-eslint/scope-manager@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.2.0.tgz#412a710d8fa20bc045533b3b19f423810b24f87a" + integrity sha512-1ZMNVgm5nnHURU8ZSJ3snsHzpFeNK84rdZjluEVBGNu7jDymfqceB3kdIZ6A4xCfEFFhRIB6rF8q/JIqJd2R0Q== dependencies: - "@typescript-eslint/types" "6.1.0" - "@typescript-eslint/visitor-keys" "6.1.0" + "@typescript-eslint/types" "6.2.0" + "@typescript-eslint/visitor-keys" "6.2.0" -"@typescript-eslint/type-utils@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.1.0.tgz#21cc6c3bc1980b03f9eb4e64580d0c5be6f08215" - integrity sha512-kFXBx6QWS1ZZ5Ni89TyT1X9Ag6RXVIVhqDs0vZE/jUeWlBv/ixq2diua6G7ece6+fXw3TvNRxP77/5mOMusx2w== +"@typescript-eslint/type-utils@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.2.0.tgz#02b27a3eeb41aa5460d6275d12cce5dd72e1c9fc" + integrity sha512-DnGZuNU2JN3AYwddYIqrVkYW0uUQdv0AY+kz2M25euVNlujcN2u+rJgfJsBFlUEzBB6OQkUqSZPyuTLf2bP5mw== dependencies: - "@typescript-eslint/typescript-estree" "6.1.0" - "@typescript-eslint/utils" "6.1.0" + "@typescript-eslint/typescript-estree" "6.2.0" + "@typescript-eslint/utils" "6.2.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.1.0.tgz#2d607c62827bb416ada5c96ebfa2ef84e45a8dfa" - integrity sha512-+Gfd5NHCpDoHDOaU/yIF3WWRI2PcBRKKpP91ZcVbL0t5tQpqYWBs3z/GGhvU+EV1D0262g9XCnyqQh19prU0JQ== +"@typescript-eslint/types@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.2.0.tgz#b341a4e6d5f609267306b07afc6f62bcf92b1495" + integrity sha512-1nRRaDlp/XYJQLvkQJG5F3uBTno5SHPT7XVcJ5n1/k2WfNI28nJsvLakxwZRNY5spuatEKO7d5nZWsQpkqXwBA== -"@typescript-eslint/typescript-estree@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.1.0.tgz#ea382f6482ba698d7e993a88ce5391ea7a66c33d" - integrity sha512-nUKAPWOaP/tQjU1IQw9sOPCDavs/iU5iYLiY/6u7gxS7oKQoi4aUxXS1nrrVGTyBBaGesjkcwwHkbkiD5eBvcg== +"@typescript-eslint/typescript-estree@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.2.0.tgz#4969944b831b481996aa4fbd73c7164ca683b8ef" + integrity sha512-Mts6+3HQMSM+LZCglsc2yMIny37IhUgp1Qe8yJUYVyO6rHP7/vN0vajKu3JvHCBIy8TSiKddJ/Zwu80jhnGj1w== dependencies: - "@typescript-eslint/types" "6.1.0" - "@typescript-eslint/visitor-keys" "6.1.0" + "@typescript-eslint/types" "6.2.0" + "@typescript-eslint/visitor-keys" "6.2.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.1.0.tgz#1641843792b4e3451cc692e2c73055df8b26f453" - integrity sha512-wp652EogZlKmQoMS5hAvWqRKplXvkuOnNzZSE0PVvsKjpexd/XznRVHAtrfHFYmqaJz0DFkjlDsGYC9OXw+OhQ== +"@typescript-eslint/utils@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.2.0.tgz#606a20e5c13883c2d2bd0538ddc4b96b8d410979" + integrity sha512-RCFrC1lXiX1qEZN8LmLrxYRhOkElEsPKTVSNout8DMzf8PeWoQG7Rxz2SadpJa3VSh5oYKGwt7j7X/VRg+Y3OQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.1.0" - "@typescript-eslint/types" "6.1.0" - "@typescript-eslint/typescript-estree" "6.1.0" + "@typescript-eslint/scope-manager" "6.2.0" + "@typescript-eslint/types" "6.2.0" + "@typescript-eslint/typescript-estree" "6.2.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.1.0.tgz#d2b84dff6b58944d3257ea03687e269a788c73be" - integrity sha512-yQeh+EXhquh119Eis4k0kYhj9vmFzNpbhM3LftWQVwqVjipCkwHBQOZutcYW+JVkjtTG9k8nrZU1UoNedPDd1A== +"@typescript-eslint/visitor-keys@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.2.0.tgz#71943f42fdaa2ec86dc3222091f41761a49ae71a" + integrity sha512-QbaYUQVKKo9bgCzpjz45llCfwakyoxHetIy8CAvYCtd16Zu1KrpzNHofwF8kGkpPOxZB2o6kz+0nqH8ZkIzuoQ== dependencies: - "@typescript-eslint/types" "6.1.0" + "@typescript-eslint/types" "6.2.0" eslint-visitor-keys "^3.4.1" "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": @@ -2167,9 +2162,9 @@ webpackbar "~5.0.2" "@xh/hoist@^59.0.0-SNAPSHOT": - version "59.0.0-SNAPSHOT.1689771665768" - resolved "https://registry.yarnpkg.com/@xh/hoist/-/hoist-59.0.0-SNAPSHOT.1689771665768.tgz#8dca6f93a7026fc2171cf42757f12f689d7bcbac" - integrity sha512-7N1tR/8OybJg69c2oOK3dURbpbn4Hmxors5KzBsUbnZZ5djFVayaUSbNr47C4DQ7Eyr4wfZ2K/yykq299z0Lyw== + version "59.0.0-SNAPSHOT.1689786597888" + resolved "https://registry.yarnpkg.com/@xh/hoist/-/hoist-59.0.0-SNAPSHOT.1689786597888.tgz#9af02de1d086495faca3af1c050f2b8feb34d4f7" + integrity sha512-gY1NR8/ETXuGmtNgK5N++wfX6LBOsVVCtNhd4A0ubO2kmgk1uGt+Fu3tYfpvpmnkHiIub9PgC4jez1/1tcwsYA== dependencies: "@blueprintjs/core" "^4.20.0" "@blueprintjs/datetime" "^4.4.35" @@ -2538,28 +2533,28 @@ babel-plugin-macros@^3.1.0: resolve "^1.19.0" babel-plugin-polyfill-corejs2@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.4.tgz#9f9a0e1cd9d645cc246a5e094db5c3aa913ccd2b" - integrity sha512-9WeK9snM1BfxB38goUEv2FLnA6ja07UMfazFHzCXUb3NyDZAwfXvQiURQ6guTTMeHcOsdknULm1PDhs4uWtKyA== + version "0.4.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz#8097b4cb4af5b64a1d11332b6fb72ef5e64a054c" + integrity sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.4.1" - "@nicolo-ribaudo/semver-v6" "^6.3.3" + "@babel/helper-define-polyfill-provider" "^0.4.2" + semver "^6.3.1" babel-plugin-polyfill-corejs3@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.2.tgz#d406c5738d298cd9c66f64a94cf8d5904ce4cc5e" - integrity sha512-Cid+Jv1BrY9ReW9lIfNlNpsI53N+FN7gE+f73zLAUbr9C52W4gKLWSByx47pfDJsEysojKArqOtOKZSVIIUTuQ== + version "0.8.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz#b4f719d0ad9bb8e0c23e3e630c0c8ec6dd7a1c52" + integrity sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.1" + "@babel/helper-define-polyfill-provider" "^0.4.2" core-js-compat "^3.31.0" babel-plugin-polyfill-regenerator@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.1.tgz#ace7a5eced6dff7d5060c335c52064778216afd3" - integrity sha512-L8OyySuI6OSQ5hFy9O+7zFjyr4WhAfRjLIOkhQGYl+emwJkd/S4XXT1JpfrgR1jrQ1NcGiOh+yAdGlF8pnC3Jw== + version "0.5.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz#80d0f3e1098c080c8b5a65f41e9427af692dc326" + integrity sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA== dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.1" + "@babel/helper-define-polyfill-provider" "^0.4.2" babel-plugin-transform-imports@~2.0.0: version "2.0.0" @@ -3419,9 +3414,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.4.431: - version "1.4.464" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.464.tgz#2f94bad78dff34e527aacbfc5d0b1a33cf046507" - integrity sha512-guZ84yoou4+ILNdj0XEbmGs6DEWj6zpVOWYpY09GU66yEb0DSYvP/biBPzHn0GuW/3RC/pnaYNUWlQE1fJYtgA== + version "1.4.471" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.471.tgz#14cb056d0ce4bfa99df57946d57fe46c2330dac3" + integrity sha512-GpmGRC1vTl60w/k6YpQ18pSiqnmr0j3un//5TV1idPi6aheNfkT1Ye71tMEabWyNDO6sBMgAR+95Eb0eUUr1tA== emoji-regex@^8.0.0: version "8.0.0" @@ -3587,9 +3582,9 @@ eslint-plugin-react-hooks@^4.6: integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.32: - version "7.32.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10" - integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg== + version "7.33.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.0.tgz#6c356fb0862fec2cd1b04426c669ea746e9b6eb3" + integrity sha512-qewL/8P34WkY8jAqdQxsiL82pDUeT7nhs8IsuXgfgnsEloKCT4miAV9N9kGtx7/KM9NH/NCGUE7Edt9iGxLXFw== dependencies: array-includes "^3.1.6" array.prototype.flatmap "^1.3.1" @@ -3604,7 +3599,7 @@ eslint-plugin-react@^7.32: object.values "^1.1.6" prop-types "^15.8.1" resolve "^2.0.0-next.4" - semver "^6.3.0" + semver "^6.3.1" string.prototype.matchall "^4.0.8" eslint-rule-composer@^0.3.0: @@ -3812,9 +3807,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.1: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" - integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -6079,9 +6074,9 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@8.x, postcss@^8.4.21, postcss@^8.4.25, postcss@~8.4.21: - version "8.4.26" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.26.tgz#1bc62ab19f8e1e5463d98cf74af39702a00a9e94" - integrity sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw== + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" @@ -6902,7 +6897,7 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -semver@^6.3.0, semver@^6.3.1: +semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -7477,9 +7472,9 @@ terser-webpack-plugin@^5.3.7, terser-webpack-plugin@~5.3.7: terser "^5.16.8" terser@^5.10.0, terser@^5.16.8: - version "5.19.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.1.tgz#dbd7231f224a9e2401d0f0959542ed74d76d340b" - integrity sha512-27hxBUVdV6GoNg1pKQ7Z5cbR6V9txPVyBA+FQw3BaZ1Wuzvztce5p156DaP0NVZNrMZZ+6iG9Syf7WgMNKDg2Q== + version "5.19.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" + integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -7549,9 +7544,9 @@ ts-api-utils@^1.0.1: integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" - integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== tslib@~2.5.0: version "2.5.3" From de7340b670a422d839f1f59c646d837a1de0b301 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 27 Jul 2023 08:05:57 -0400 Subject: [PATCH 3/8] Support removing item from msg history --- client-app/src/core/svc/ChatGptService.ts | 4 +++ client-app/src/examples/chat/ChatModel.ts | 34 ++++++++++++++++++----- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts index ff45636ac..c1827bd4a 100644 --- a/client-app/src/core/svc/ChatGptService.ts +++ b/client-app/src/core/svc/ChatGptService.ts @@ -282,6 +282,10 @@ export class ChatGptService extends HoistService { this.userMessageHistory = history; } + removeFromMessageHistory(msg: string) { + this.userMessageHistory = this.userMessageHistory.filter(it => it !== msg); + } + clearUserMessageHistory() { this.userMessageHistory = []; } diff --git a/client-app/src/examples/chat/ChatModel.ts b/client-app/src/examples/chat/ChatModel.ts index a5582fdbc..15ab927fe 100644 --- a/client-app/src/examples/chat/ChatModel.ts +++ b/client-app/src/examples/chat/ChatModel.ts @@ -5,6 +5,8 @@ import {createObservableRef} from '@xh/hoist/utils/react'; import {HoistInputModel} from '@xh/hoist/cmp/input'; import {isEmpty, last} from 'lodash'; import {wait} from '@xh/hoist/promise'; +import {actionCol} from '@xh/hoist/desktop/cmp/grid'; +import {Icon} from '@xh/hoist/icon'; export class ChatModel extends HoistModel { @bindable inputMsg: string; @@ -22,7 +24,9 @@ export class ChatModel extends HoistModel { scrollRef = createObservableRef(); - async initAsync() { + constructor() { + super(); + this.userHistoryGridModel = this.createUsersHistoryGridModel(); this.addReaction( @@ -86,7 +90,7 @@ export class ChatModel extends HoistModel { async clearAndReInitAsync() { await XH.chatGptService.clearAndReInitAsync(); - XH.toast('Chat history cleared.'); + XH.toast({message: 'Chat history cleared.', position: 'top'}); this.focusInput(); } @@ -114,11 +118,27 @@ export class ChatModel extends HoistModel { hideHeaders: true, stripeRows: true, rowBorders: true, - columns: [{field: 'message', flex: 1}], - onRowClicked: ({data: record}) => { - this.showUserMessageHistory = false; - this.inputMsg = record.data.message; - this.focusInput(); + columns: [ + {field: 'message', flex: 1}, + { + ...actionCol, + actions: [ + { + icon: Icon.x(), + intent: 'danger', + actionFn: ({record}) => { + XH.chatGptService.removeFromMessageHistory(record.data.message); + } + } + ] + } + ], + onCellClicked: ({data: record, column}) => { + if (column.colId === 'message') { + this.showUserMessageHistory = false; + this.inputMsg = record.data.message; + this.focusInput(); + } } }); } From 7e927e9445f86ef6cc0158a5633a38c78f61914c Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 27 Jul 2023 10:54:43 -0400 Subject: [PATCH 4/8] WIP --- client-app/src/core/svc/ChatGptService.ts | 36 +++++++++++++---- client-app/src/examples/chat/AppComponent.ts | 3 +- .../src/examples/chat/{ => cmp}/Chat.scss | 4 ++ .../src/examples/chat/{ => cmp}/ChatModel.ts | 4 +- .../src/examples/chat/{ => cmp}/ChatPanel.ts | 5 ++- .../chat/cmp/{ => impl}/MessageList.ts | 25 ++++++++---- .../chat/cmp/{ => impl}/PromptInput.ts | 0 .../chat/cmp/impl/StructResponseModel.ts | 40 +++++++++++++++++++ .../chat/cmp/impl/StructResponsePanel.ts | 28 +++++++++++++ .../chat/{ => docs}/system-message.md | 0 10 files changed, 124 insertions(+), 21 deletions(-) rename client-app/src/examples/chat/{ => cmp}/Chat.scss (96%) rename client-app/src/examples/chat/{ => cmp}/ChatModel.ts (97%) rename client-app/src/examples/chat/{ => cmp}/ChatPanel.ts (88%) rename client-app/src/examples/chat/cmp/{ => impl}/MessageList.ts (76%) rename client-app/src/examples/chat/cmp/{ => impl}/PromptInput.ts (100%) create mode 100644 client-app/src/examples/chat/cmp/impl/StructResponseModel.ts create mode 100644 client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts rename client-app/src/examples/chat/{ => docs}/system-message.md (100%) diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts index c1827bd4a..d6bc8d616 100644 --- a/client-app/src/core/svc/ChatGptService.ts +++ b/client-app/src/core/svc/ChatGptService.ts @@ -1,13 +1,16 @@ import {HoistService, persist, XH} from '@xh/hoist/core'; -import {dropRight, isEmpty, isString, last, remove} from 'lodash'; +import {dropRight, isEmpty, isString, last, pick, remove} from 'lodash'; import {bindable, makeObservable, observable} from '@xh/hoist/mobx'; import {logInfo, withInfo} from '@xh/hoist/utils/js'; +import type {SetOptional} from 'type-fest'; export interface GptMessage { role: 'system' | 'user' | 'assistant' | 'function'; + timestamp: number; content?: string; name?: string; function_call?: GptFnCallResponse; + responseJson?: string; } export interface GptFnCallResponse { @@ -160,7 +163,7 @@ export class ChatGptService extends HoistService { super(); makeObservable(this); - this.messages.forEach(msg => this.logMsg(msg)); + // this.messages.forEach(msg => this.logMsg(msg)); this.addReaction( { @@ -225,13 +228,20 @@ export class ChatGptService extends HoistService { await this.initAsync(); } - async sendChatAsync(message: GptMessage | string, options: GptChatOptions = {}) { + async sendChatAsync( + message: SetOptional | string, + options: GptChatOptions = {} + ) { const msgToSend: GptMessage = isString(message) ? { role: 'user', - content: message + content: message, + timestamp: Date.now() } - : message; + : { + timestamp: Date.now(), + ...message + }; // Push user message onto state immediately, to indicate that it's been sent. this.messages = [...this.messages, msgToSend]; @@ -243,7 +253,7 @@ export class ChatGptService extends HoistService { const body = { model: this.model, - messages: this.messages, + messages: this.messages.map(it => this.formatMessageForPost(it)), functions: this.functions, ...options }; @@ -269,8 +279,18 @@ export class ChatGptService extends HoistService { console.debug(resp); if (isEmpty(resp?.choices)) throw XH.exception('GPT did not return any choices'); - const gptReply = resp.choices[0]; - this.messages = [...this.messages, gptReply.message]; + const gptReplyChoice = resp.choices[0], + gptResponse: GptMessage = { + ...gptReplyChoice.message, + timestamp: Date.now(), + responseJson: JSON.stringify(resp, null, 2) + }; + this.messages = [...this.messages, gptResponse]; + } + + // Strip any extra fields from message before sending to GPT. + formatMessageForPost(msg: GptMessage) { + return pick(msg, ['role', 'content', 'name', 'function_call']); } updateUserMessageHistory(msg: string) { diff --git a/client-app/src/examples/chat/AppComponent.ts b/client-app/src/examples/chat/AppComponent.ts index 9583f0135..1fe0f1e6a 100644 --- a/client-app/src/examples/chat/AppComponent.ts +++ b/client-app/src/examples/chat/AppComponent.ts @@ -10,9 +10,8 @@ import {div, fragment} from '@xh/hoist/cmp/layout'; import {jsonInput, select, switchInput} from '@xh/hoist/desktop/cmp/input'; import {button} from '@xh/hoist/desktop/cmp/button'; import ReactMarkdown from 'react-markdown'; -import './Chat.scss'; import {popover} from '@xh/hoist/kit/blueprint'; -import {chatPanel} from './ChatPanel'; +import {chatPanel} from './cmp/ChatPanel'; library.add(faPaperPlane, faRobot, faUserRobotXmarks); diff --git a/client-app/src/examples/chat/Chat.scss b/client-app/src/examples/chat/cmp/Chat.scss similarity index 96% rename from client-app/src/examples/chat/Chat.scss rename to client-app/src/examples/chat/cmp/Chat.scss index 8f8d0e2c5..0ac4fced4 100644 --- a/client-app/src/examples/chat/Chat.scss +++ b/client-app/src/examples/chat/cmp/Chat.scss @@ -11,6 +11,10 @@ background-color: var(--xh-grid-bg-odd); } + &--selected { + outline: 1px solid var(--xh-orange); + } + &__avatar { color: var(--xh-orange-muted); width: 40px; diff --git a/client-app/src/examples/chat/ChatModel.ts b/client-app/src/examples/chat/cmp/ChatModel.ts similarity index 97% rename from client-app/src/examples/chat/ChatModel.ts rename to client-app/src/examples/chat/cmp/ChatModel.ts index 15ab927fe..33a9db650 100644 --- a/client-app/src/examples/chat/ChatModel.ts +++ b/client-app/src/examples/chat/cmp/ChatModel.ts @@ -7,10 +7,12 @@ import {isEmpty, last} from 'lodash'; import {wait} from '@xh/hoist/promise'; import {actionCol} from '@xh/hoist/desktop/cmp/grid'; import {Icon} from '@xh/hoist/icon'; +import {GptMessage} from '../../../core/svc/ChatGptService'; export class ChatModel extends HoistModel { @bindable inputMsg: string; - @bindable showFunctionEditor: boolean = false; + + @bindable.ref selectedMsg: GptMessage; @bindable showUserMessageHistory = false; @managed userHistoryGridModel: GridModel; diff --git a/client-app/src/examples/chat/ChatPanel.ts b/client-app/src/examples/chat/cmp/ChatPanel.ts similarity index 88% rename from client-app/src/examples/chat/ChatPanel.ts rename to client-app/src/examples/chat/cmp/ChatPanel.ts index 1f74c7137..1c5a33079 100644 --- a/client-app/src/examples/chat/ChatPanel.ts +++ b/client-app/src/examples/chat/cmp/ChatPanel.ts @@ -1,11 +1,12 @@ import {creates, hoistCmp, XH} from '@xh/hoist/core'; import {ChatModel} from './ChatModel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; -import {messageList} from './cmp/MessageList'; -import {promptInput} from './cmp/PromptInput'; +import {messageList} from './impl/MessageList'; +import {promptInput} from './impl/PromptInput'; import {placeholder} from '@xh/hoist/cmp/layout'; import {Icon} from '@xh/hoist/icon'; import {button} from '@xh/hoist/desktop/cmp/button'; +import './Chat.scss'; export const chatPanel = hoistCmp.factory({ displayName: 'ChatPanel', diff --git a/client-app/src/examples/chat/cmp/MessageList.ts b/client-app/src/examples/chat/cmp/impl/MessageList.ts similarity index 76% rename from client-app/src/examples/chat/cmp/MessageList.ts rename to client-app/src/examples/chat/cmp/impl/MessageList.ts index c6eb9528e..8688b921d 100644 --- a/client-app/src/examples/chat/cmp/MessageList.ts +++ b/client-app/src/examples/chat/cmp/impl/MessageList.ts @@ -4,9 +4,10 @@ import {box, div, hbox, img, placeholder} from '@xh/hoist/cmp/layout'; import {Icon} from '@xh/hoist/icon'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {errorMessage} from '@xh/hoist/desktop/cmp/error'; -import {GptMessage} from '../../../core/svc/ChatGptService'; +import {GptMessage} from '../../../../core/svc/ChatGptService'; import ReactMarkdown from 'react-markdown'; import {ChatModel} from '../ChatModel'; +import {structResponsePanel} from './StructResponsePanel'; export const messageList = hoistCmp.factory({ displayName: 'MessageList', @@ -16,12 +17,18 @@ export const messageList = hoistCmp.factory({ const {messages} = XH.chatGptService, item = isEmpty(messages) ? placeholder(Icon.ellipsisHorizontal(), 'No messages yet...') - : div({ - className: 'tb-msg-list', + : hbox({ + flex: 1, items: [ - ...messages.map(message => msgItem({message})), - // Supports scrolling to bottom of list. - box({ref: model.scrollRef}) + div({ + className: 'tb-msg-list', + items: [ + ...messages.map(message => msgItem({message})), + // Supports scrolling to bottom of list. + box({ref: model.scrollRef}) + ] + }), + structResponsePanel({}) ] }); @@ -35,6 +42,7 @@ export const messageList = hoistCmp.factory({ const msgItem = hoistCmp.factory({ render({model, message}) { const {role, content, function_call} = message, + isSelected = model.selectedMsg === message, items = []; // System message is visible via popover from top toolbar. @@ -59,14 +67,15 @@ const msgItem = hoistCmp.factory({ } return hbox({ - className: `tb-msg`, + className: `tb-msg ${isSelected ? 'tb-msg--selected' : ''}`, items: [ avatar({role}), div({ className: 'tb-msg__content', items }) - ] + ], + onClick: () => (model.selectedMsg = message) }); } }); diff --git a/client-app/src/examples/chat/cmp/PromptInput.ts b/client-app/src/examples/chat/cmp/impl/PromptInput.ts similarity index 100% rename from client-app/src/examples/chat/cmp/PromptInput.ts rename to client-app/src/examples/chat/cmp/impl/PromptInput.ts diff --git a/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts new file mode 100644 index 000000000..c4ce47aab --- /dev/null +++ b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts @@ -0,0 +1,40 @@ +import {HoistModel, lookup, XH} from '@xh/hoist/core'; +import {ChatModel} from '../ChatModel'; +import {GptMessage} from '../../../../core/svc/ChatGptService'; +import {GridModel} from '@xh/hoist/cmp/grid'; +import {bindable} from '@xh/hoist/mobx'; + +export class StructResponseModel extends HoistModel { + @lookup(ChatModel) chatModel: ChatModel; + + get selectedMsg(): GptMessage { + return this.chatModel?.selectedMsg; + } + + get shouldDisplay() { + return this.selectedMsg?.function_call != null; + } + + override onLinked() { + this.addReaction({ + track: () => this.selectedMsg, + run: () => this.onSelectedMsgChange(), + fireImmediately: true + }); + } + + @bindable title: string; + @bindable.ref gridModel: GridModel; + + onSelectedMsgChange() { + const {selectedMsg} = this; + + if (!selectedMsg) { + this.title = 'Select a GPT response to view associated data'; + XH.safeDestroy(this.gridModel); + this.gridModel = null; + } else { + this.title = 'Some generated title'; + } + } +} diff --git a/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts b/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts new file mode 100644 index 000000000..42c37de85 --- /dev/null +++ b/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts @@ -0,0 +1,28 @@ +import {creates, hoistCmp} from '@xh/hoist/core'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {StructResponseModel} from './StructResponseModel'; +import {placeholder} from '@xh/hoist/cmp/layout'; +import {Icon} from '@xh/hoist/icon'; + +export const structResponsePanel = hoistCmp.factory({ + displayName: 'StructuredResponsePanel', + model: creates(StructResponseModel), + + render({model, ...rest}) { + return panel({ + title: model.title, + compactHeader: true, + item: model.shouldDisplay + ? 'TODO' + : placeholder( + Icon.grid(), + 'Select a GPT response with structured data attached to view the results.' + ), + modelConfig: { + defaultSize: '30%', + side: 'right' + }, + ...rest + }); + } +}); diff --git a/client-app/src/examples/chat/system-message.md b/client-app/src/examples/chat/docs/system-message.md similarity index 100% rename from client-app/src/examples/chat/system-message.md rename to client-app/src/examples/chat/docs/system-message.md From 634bcd6706b9579f094fc72ddbc3f8d8ce1e33b3 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 27 Jul 2023 18:35:01 -0400 Subject: [PATCH 5/8] WIP with structured data output --- client-app/src/core/svc/ChatGptService.ts | 1 + client-app/src/examples/chat/cmp/Chat.scss | 4 +- client-app/src/examples/chat/cmp/ChatModel.ts | 11 ++ .../chat/cmp/impl/StructResponseModel.ts | 118 ++++++++++++++++-- .../chat/cmp/impl/StructResponsePanel.ts | 22 +++- 5 files changed, 143 insertions(+), 13 deletions(-) diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts index d6bc8d616..c588c1cbb 100644 --- a/client-app/src/core/svc/ChatGptService.ts +++ b/client-app/src/core/svc/ChatGptService.ts @@ -251,6 +251,7 @@ export class ChatGptService extends HoistService { this.updateUserMessageHistory(msgToSend.content); } + console.log(this.messages.map(it => this.formatMessageForPost(it))); const body = { model: this.model, messages: this.messages.map(it => this.formatMessageForPost(it)), diff --git a/client-app/src/examples/chat/cmp/Chat.scss b/client-app/src/examples/chat/cmp/Chat.scss index 0ac4fced4..22cf251f2 100644 --- a/client-app/src/examples/chat/cmp/Chat.scss +++ b/client-app/src/examples/chat/cmp/Chat.scss @@ -11,8 +11,10 @@ background-color: var(--xh-grid-bg-odd); } + border-left: 4px solid transparent; &--selected { - outline: 1px solid var(--xh-orange); + border-left-color: var(--xh-orange); + background-color: var(--xh-intent-primary-trans1) !important; } &__avatar { diff --git a/client-app/src/examples/chat/cmp/ChatModel.ts b/client-app/src/examples/chat/cmp/ChatModel.ts index 33a9db650..6c7de10ab 100644 --- a/client-app/src/examples/chat/cmp/ChatModel.ts +++ b/client-app/src/examples/chat/cmp/ChatModel.ts @@ -14,6 +14,16 @@ export class ChatModel extends HoistModel { @bindable.ref selectedMsg: GptMessage; + /** If the currently selected message is a GPT response, show the preceding user message. */ + get userPromptForSelectedMsg() { + const {selectedMsg} = this; + if (!selectedMsg || selectedMsg.role !== 'assistant') return null; + + // TODO - assumes user/assistant messages are always alternating + const msgIdx = XH.chatGptService.messages.indexOf(selectedMsg); + return XH.chatGptService.messages[msgIdx - 1]; + } + @bindable showUserMessageHistory = false; @managed userHistoryGridModel: GridModel; @@ -108,6 +118,7 @@ export class ChatModel extends HoistModel { scrollMessages() { wait(500).then(() => { this.scrollRef.current?.scrollIntoView({behavior: 'auto'}); + this.selectedMsg = last(XH.chatGptService.messages); }); } diff --git a/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts index c4ce47aab..fe8e2a641 100644 --- a/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts +++ b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts @@ -1,8 +1,11 @@ -import {HoistModel, lookup, XH} from '@xh/hoist/core'; +import {HoistModel, lookup, PlainObject, XH} from '@xh/hoist/core'; import {ChatModel} from '../ChatModel'; import {GptMessage} from '../../../../core/svc/ChatGptService'; import {GridModel} from '@xh/hoist/cmp/grid'; import {bindable} from '@xh/hoist/mobx'; +import {forOwn, isEmpty, isNil, isNumber, orderBy, take} from 'lodash'; +import {numberRenderer} from '@xh/hoist/format'; +import {mktValCol, pnlCol} from '../../../../core/columns'; export class StructResponseModel extends HoistModel { @lookup(ChatModel) chatModel: ChatModel; @@ -15,26 +18,125 @@ export class StructResponseModel extends HoistModel { return this.selectedMsg?.function_call != null; } + get title() { + return this.shouldDisplay + ? this.chatModel.userPromptForSelectedMsg?.content ?? + this.selectedMsg?.function_call.name + : null; + } + override onLinked() { this.addReaction({ track: () => this.selectedMsg, - run: () => this.onSelectedMsgChange(), + run: () => this.onMsgChangeAsync(), fireImmediately: true }); } - @bindable title: string; + // Set based on data extracted from selectedMsg @bindable.ref gridModel: GridModel; + @bindable.ref data: PlainObject[]; - onSelectedMsgChange() { + async onMsgChangeAsync() { const {selectedMsg} = this; + XH.safeDestroy(this.gridModel); + this.gridModel = null; + if (!selectedMsg) { - this.title = 'Select a GPT response to view associated data'; - XH.safeDestroy(this.gridModel); - this.gridModel = null; } else { - this.title = 'Some generated title'; + const data = await this.getDataAsync(selectedMsg), + gridModel = this.createGridModel(data, selectedMsg); + + gridModel?.loadData(data); + + XH.safeDestroy(this.gridModel); + this.data = data; + this.gridModel = gridModel; } } + + async getDataAsync(msg: GptMessage) { + const {function_call} = msg; + if (!function_call) return null; + + let data; + switch (function_call.name) { + case 'getPortfolioPositions': + data = await this.getPortfolioPositionsAsync(msg); + break; + default: + throw XH.exception(`Unsupported function call: ${function_call.name}`); + } + + const args = this.getArgs(msg); + console.log(args); + console.log(data); + if (args.maxRows && data.length > args.maxRows) { + console.log(`truncating to ${args.maxRows} rows`); + if (args.sortBy) { + const sortFieldAndDir = args.sortBy.split('|'); + data = orderBy(data, [sortFieldAndDir[0]], [sortFieldAndDir[1]]); + } + + data = take(data, args.maxRows); + } + + return data; + } + + async getPortfolioPositionsAsync(msg: GptMessage) { + const args = this.getArgs(msg), + {groupByDimensions} = args; + return XH.portfolioService.getPositionsAsync(groupByDimensions, false); + } + + createGridModel(data: PlainObject[], msg: GptMessage) { + if (isEmpty(data)) return null; + + try { + const args = this.getArgs(msg), + columns = [], + skippedKeys = ['children', 'id']; + + forOwn(data[0], (value, key) => { + if (!skippedKeys.includes(key)) { + const isNum = this.isNumberField(data, key), + colDef = this.cols[key] ?? {}; + + columns.push({ + field: {name: key, type: isNum ? 'number' : 'auto'}, + isTreeColumn: key === 'name', + renderer: isNum ? numberRenderer() : null, + ...colDef + }); + } + }); + + return new GridModel({ + autosizeOptions: {mode: 'managed'}, + store: {idSpec: XH.genId}, + treeMode: data[0].hasOwnProperty('children'), + sortBy: args.sortBy ?? 'name', + columns + }); + } catch (e) { + XH.handleException(e); + return null; + } + } + + // Rough heuristic - doesn't attempt to recurse into children, etc. + isNumberField(data: PlainObject[], key: string) { + return data.every(it => isNil(it[key]) || isNumber(it[key])); + } + + getArgs(msg: GptMessage): PlainObject { + return JSON.parse(msg.function_call.arguments); + } + + cols = { + pnl: pnlCol, + mktVal: mktValCol + }; } diff --git a/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts b/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts index 42c37de85..2293496b9 100644 --- a/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts +++ b/client-app/src/examples/chat/cmp/impl/StructResponsePanel.ts @@ -3,26 +3,40 @@ import {panel} from '@xh/hoist/desktop/cmp/panel'; import {StructResponseModel} from './StructResponseModel'; import {placeholder} from '@xh/hoist/cmp/layout'; import {Icon} from '@xh/hoist/icon'; +import {grid} from '@xh/hoist/cmp/grid'; export const structResponsePanel = hoistCmp.factory({ displayName: 'StructuredResponsePanel', model: creates(StructResponseModel), render({model, ...rest}) { + const {title, shouldDisplay} = model; return panel({ - title: model.title, + title, + icon: title ? Icon.terminal() : null, compactHeader: true, - item: model.shouldDisplay - ? 'TODO' + item: shouldDisplay + ? dataComponent() : placeholder( Icon.grid(), 'Select a GPT response with structured data attached to view the results.' ), modelConfig: { - defaultSize: '30%', + defaultSize: 450, side: 'right' }, ...rest }); } }); + +const dataComponent = hoistCmp.factory({ + render({model}) { + const {gridModel} = model; + if (gridModel) { + return grid({model: gridModel, flex: 1, agOptions: {groupDefaultExpanded: 1}}); + } + + return placeholder('No structured data found.'); + } +}); From 6d77c2a29ac1606aa6d6e7a38c8f0953fdacf6cc Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Wed, 23 Aug 2023 06:30:08 -0700 Subject: [PATCH 6/8] Avoid NPE, fix TS warning --- client-app/src/examples/chat/cmp/ChatModel.ts | 2 +- client-app/src/examples/chat/cmp/impl/StructResponseModel.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client-app/src/examples/chat/cmp/ChatModel.ts b/client-app/src/examples/chat/cmp/ChatModel.ts index 6c7de10ab..52ad3735b 100644 --- a/client-app/src/examples/chat/cmp/ChatModel.ts +++ b/client-app/src/examples/chat/cmp/ChatModel.ts @@ -147,7 +147,7 @@ export class ChatModel extends HoistModel { } ], onCellClicked: ({data: record, column}) => { - if (column.colId === 'message') { + if (column.getColId() === 'message') { this.showUserMessageHistory = false; this.inputMsg = record.data.message; this.focusInput(); diff --git a/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts index fe8e2a641..08e25a2a2 100644 --- a/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts +++ b/client-app/src/examples/chat/cmp/impl/StructResponseModel.ts @@ -21,7 +21,7 @@ export class StructResponseModel extends HoistModel { get title() { return this.shouldDisplay ? this.chatModel.userPromptForSelectedMsg?.content ?? - this.selectedMsg?.function_call.name + this.selectedMsg?.function_call?.name : null; } From 08301b0de3b5033fcf7e409a6cc20ec6b6598855 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 21 Sep 2023 17:25:39 -0700 Subject: [PATCH 7/8] WIP --- client-app/src/core/svc/ChatGptService.ts | 49 +++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts index c588c1cbb..4fda29e14 100644 --- a/client-app/src/core/svc/ChatGptService.ts +++ b/client-app/src/core/svc/ChatGptService.ts @@ -34,7 +34,7 @@ export class ChatGptService extends HoistService { override persistWith = {localStorageKey: 'chatGptService'}; // Initialized from config via dedicated server call. - // Configs are protected and not sent to all clients - the CHAT_GPT_USER role required. + // Configs are protected and not sent to all clients - the CHAT_GPT_USER role is required. apiKey: string; completionUrl: string; @@ -102,6 +102,8 @@ export class ChatGptService extends HoistService { '\n' + '```typescript\n' + 'interface RawPosition {\n' + + ' // Dimension - the trading strategy to which this position belongs. The value will be a code returned by the `findStrategies` function.\n' + + ' strategy: string;\n' + " // Dimension - the stock ticker or identifier of the position's instrument, an equity stock or other security - e.g. ['AAPL', 'GOOG', 'MSFT']\n" + ' symbol: string;\n' + " // Dimension - the industry sector of the instrument - e.g. ['Technology', 'Healthcare', 'Energy']\n" + @@ -120,13 +122,20 @@ export class ChatGptService extends HoistService { 'The `getPortfolioPositions` function takes a list of `groupByDimensions` when aggregating results, representing\n' + 'the field names of `RawPosition` dimensions within the portfolio data.\n' + '\n' + - 'Introduce yourself to the user and ask them how you can help them.\n'; + 'A common way to filter positions is to '; + 'Introduce yourself to the user and ask them how you can help them.\n'; functions = [ { name: 'getPortfolioPositions', description: - 'Query a portfolio of `RawPosition` objects representing investments to return aggregated `Position` objects with P&L (profit and loss) and market value data, grouped by one or more specified dimensions. Each grouped row in the return will have the following properties: `name`, `pnl` (profit and loss), and `mktVal` (market value). If multiple grouping dimensions are specified, the results will be returned in a tree structure, where each parent group will have a `children` property containing an array of nested sub-groups.', + 'Query a portfolio of `RawPosition` objects representing investments to return aggregated `Position` ' + + 'objects with P&L (profit and loss) and market value data, grouped by one or more specified dimensions. ' + + 'Each grouped row in the return will have the following properties: `name`, `pnl` (profit and loss), and ' + + '`mktVal` (market value). If multiple grouping dimensions are specified, the results will be returned in ' + + 'a tree structure, where each parent group will have a `children` property containing an array of nested sub-groups. ' + + 'Many queries should be run by first finding a list of strategy codes using the `findStrategies` function, ' + + 'then passing those codes to the `strategies` parameter in this function.', parameters: { type: 'object', properties: { @@ -152,10 +161,44 @@ export class ChatGptService extends HoistService { 'The maximum number of top-level rows to return. Leave unspecified to return all available groupings.', type: 'integer', minimum: 1 + }, + strategies: { + type: 'array', + items: { + type: 'string' + }, + description: + 'Optional list of strategy codes to filter by. Strategy codes should be first looked up via the `findStrategies` function, then used in this parameter to find suitable positions.' } }, required: ['groupByDimensions'] } + }, + { + name: 'findStrategies', + description: + 'Search for suitable strategy codes that can then be used in portfolio queries.', + parameters: { + type: 'object', + properties: { + sector: { + type: 'string', + description: + 'Optional filter by sector, the econonmic area or industry covered by a strategy. e.g. "Healthcare" or "Technology".' + }, + analyst: { + type: 'string', + description: + 'Optional filter by lead analyst, the individual person who is responsible for managing the strategy. e.g. "Susan Major".' + }, + freeText: { + type: 'string', + description: + 'Optional free text search that can be used to match across multiple strategy fields, when you are unsure which specific field or fields to filter on.' + } + }, + minProperties: 1 + } } ]; From 13783c7fd396ef65e0a6cde254b8fe8d5e22b061 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Tue, 3 Oct 2023 10:52:09 -0700 Subject: [PATCH 8/8] Prompt fix --- client-app/src/core/svc/ChatGptService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client-app/src/core/svc/ChatGptService.ts b/client-app/src/core/svc/ChatGptService.ts index 4fda29e14..f329c3f94 100644 --- a/client-app/src/core/svc/ChatGptService.ts +++ b/client-app/src/core/svc/ChatGptService.ts @@ -122,8 +122,7 @@ export class ChatGptService extends HoistService { 'The `getPortfolioPositions` function takes a list of `groupByDimensions` when aggregating results, representing\n' + 'the field names of `RawPosition` dimensions within the portfolio data.\n' + '\n' + - 'A common way to filter positions is to '; - 'Introduce yourself to the user and ask them how you can help them.\n'; + 'Begin by introducing yourself to the user and ask them how you can help them.\n'; functions = [ {