From bb36dbc4b9f6dcb54a58995378678b799ef6bdae Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 21 Nov 2023 21:31:21 -0800 Subject: [PATCH 01/44] Removed the Labs page, removed a store --- docs/changelog.md | 2 +- pages/labs.tsx | 14 ---- .../components/applayout/ChatDrawerItems.tsx | 6 +- .../chat/components/composer/ChatModeMenu.tsx | 6 +- .../chat/components/composer/Composer.tsx | 3 +- .../persona-selector/PersonaSelector.tsx | 7 +- src/apps/labs/AppLabs.tsx | 68 ------------------- src/apps/settings-modal/UxLabsSettings.tsx | 51 +++++++------- src/common/app.routes.ts | 3 - .../components/forms/FormSwitchControl.tsx | 10 +-- src/common/components/forms/useFormRadio.tsx | 9 +-- src/common/state/store-ui.ts | 68 ++++++++----------- src/common/state/store-ux-labs.ts | 44 ++++++++++++ .../llms/vendors/openai/OpenAISourceSetup.tsx | 4 +- 14 files changed, 121 insertions(+), 174 deletions(-) delete mode 100644 pages/labs.tsx delete mode 100644 src/apps/labs/AppLabs.tsx create mode 100644 src/common/state/store-ux-labs.ts diff --git a/docs/changelog.md b/docs/changelog.md index 1b2a389a25..ea707938e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -35,7 +35,7 @@ by release. - **Backup/Restore** - save chats, and restore them later - **[Local model support with Oobabooga server](../docs/config-local-oobabooga)** - run your own LLMs! - **Flatten conversations** - conversations summarizer with 4 modes -- **Fork conversations** - create a new chat, to experiment with different endings +- **Fork conversations** - create a new chat, to try with different endings - New commands: /s to add a System message, and /a for an Assistant message - New Chat modes: Write-only - just appends the message, without assistant response - Fix STOP generation - in sync with the Vercel team to fix a long-standing NextJS issue diff --git a/pages/labs.tsx b/pages/labs.tsx deleted file mode 100644 index 0efb28f6fd..0000000000 --- a/pages/labs.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -import { AppLabs } from '../src/apps/labs/AppLabs'; - -import { AppLayout } from '~/common/layout/AppLayout'; - - -export default function LabsPage() { - return ( - - - - ); -} \ No newline at end of file diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 3bc539ebcf..824a4d36ec 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -10,6 +10,7 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats'; import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon'; import { closeLayoutDrawer } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; +import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ConversationItem } from './ConversationItem'; @@ -35,7 +36,8 @@ export function ChatDrawerItems(props: { conversationIDs: state.conversations.map(_c => _c.id), maxChatMessages: state.conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 0), }), shallow); - const [experimentalLabs, showSymbols] = useUIPreferencesStore(state => [state.experimentalLabs, state.zenMode !== 'cleaner'], shallow); + const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner'); + const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI); // derived state const totalConversations = conversationIDs.length; @@ -119,7 +121,7 @@ export function ChatDrawerItems(props: { conversationId={conversationId} isActive={conversationId === props.conversationId} isLonely={singleChat} - maxChatMessages={(experimentalLabs || softMaxReached) ? maxChatMessages : 0} + maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0} showSymbols={showSymbols} onConversationActivate={handleConversationActivate} onConversationDelete={handleConversationDelete} diff --git a/src/apps/chat/components/composer/ChatModeMenu.tsx b/src/apps/chat/components/composer/ChatModeMenu.tsx index c5c5f5cefb..0183352649 100644 --- a/src/apps/chat/components/composer/ChatModeMenu.tsx +++ b/src/apps/chat/components/composer/ChatModeMenu.tsx @@ -5,6 +5,7 @@ import { Box, MenuItem, Radio, Typography } from '@mui/joy'; import { CloseableMenu } from '~/common/components/CloseableMenu'; import { KeyStroke } from '~/common/components/KeyStroke'; import { useUIPreferencesStore } from '~/common/state/store-ui'; +import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { ChatModeId } from '../../AppChat'; @@ -48,10 +49,11 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) { return shortcut; } -export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, experimental: boolean, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) { +export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) { // external state const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline); + const labsMagicDraw = useUXLabsStore(state => state.labsMagicDraw); return props.experimental || !experimental) + .filter(([, { experimental }]) => labsMagicDraw || !experimental) .map(([key, data]) => props.onSetChatModeId(key as ChatModeId)}> diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index 13922b6439..bfe2f7d8ef 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -137,7 +137,7 @@ export function Composer(props: { const isMobile = useIsMobile(); const [chatModeId, setChatModeId] = React.useState('immediate'); const [startupText, setStartupText] = useComposerStartupText(); - const [enterIsNewline, experimentalLabs] = useUIPreferencesStore(state => [state.enterIsNewline, state.experimentalLabs], shallow); + const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline); const { assistantTyping, systemPurposeId, tokenCount: conversationTokenCount, stopTyping } = useChatStore(state => { const conversation = state.conversations.find(_c => _c.id === props.conversationId); return { @@ -638,7 +638,6 @@ export function Composer(props: { {!!chatModeMenuAnchor && ( )} diff --git a/src/apps/chat/components/persona-selector/PersonaSelector.tsx b/src/apps/chat/components/persona-selector/PersonaSelector.tsx index 71579e29bc..523e0cf8ce 100644 --- a/src/apps/chat/components/persona-selector/PersonaSelector.tsx +++ b/src/apps/chat/components/persona-selector/PersonaSelector.tsx @@ -10,6 +10,7 @@ import TelegramIcon from '@mui/icons-material/Telegram'; import { DConversationId, useChatStore } from '~/common/state/store-chats'; import { Link } from '~/common/components/Link'; import { useUIPreferencesStore } from '~/common/state/store-ui'; +import { useUXLabsStore } from '~/common/state/store-ux-labs'; import { SystemPurposeId, SystemPurposes } from '../../../../data'; import { usePurposeStore } from './store-purposes'; @@ -46,6 +47,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa // external state const showFinder = useUIPreferencesStore(state => state.showPurposeFinder); + const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator); const { systemPurposeId, setSystemPurposeId } = useChatStore(state => { const conversation = state.conversations.find(conversation => conversation.id === props.conversationId); return { @@ -184,7 +186,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa ))} {/* Button to start the YouTube persona creator */} - + {labsPersonaYTCreator && - + } - ({ - experimentalLabs: state.experimentalLabs, setExperimentalLabs: state.setExperimentalLabs, - }), shallow); - - const handleLabsChange = (event: React.ChangeEvent) => setExperimentalLabs(event.target.checked); - - return ( - - - - - Labs - - - - - - - - - The Labs section is where we experiment with new features and ideas. - - - Features {experimentalLabs ? 'enabled' : 'disabled'}: - -
    -
  • Text tools - complete (highlight differences)
  • -
  • YouTube persona synthesizer - alpha, not persisted
  • -
  • Chat mode: follow-up/augmentation - alpha (diagrams)
  • -
  • Relative chats size - complete
  • -
- - For any questions and creative idea, please join us on Discord, and let's talk! - -
-
-
- - - -
- ); -} \ No newline at end of file diff --git a/src/apps/settings-modal/UxLabsSettings.tsx b/src/apps/settings-modal/UxLabsSettings.tsx index bbdfdd189a..4a9f91745f 100644 --- a/src/apps/settings-modal/UxLabsSettings.tsx +++ b/src/apps/settings-modal/UxLabsSettings.tsx @@ -1,41 +1,40 @@ import * as React from 'react'; -import { shallow } from 'zustand/shallow'; -import { Button, FormControl, Switch } from '@mui/joy'; +import { FormControl, Typography } from '@mui/joy'; import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; -import { closeLayoutPreferences } from '~/common/layout/store-applayout'; -import { navigateToLabs } from '~/common/app.routes'; -import { useUIPreferencesStore } from '~/common/state/store-ui'; +import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl'; +import { useUXLabsStore } from '~/common/state/store-ux-labs'; export function UxLabsSettings() { - // external state - const { - experimentalLabs, setExperimentalLabs, - } = useUIPreferencesStore(state => ({ - experimentalLabs: state.experimentalLabs, setExperimentalLabs: state.setExperimentalLabs, - }), shallow); - - const handleExperimentalLabsChange = (event: React.ChangeEvent) => setExperimentalLabs(event.target.checked); + // external state + const { /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator } = useUXLabsStore(); return <> - - - + + + {/**/} + + + + + + + Auto Diagrams · Relative chat size · Text Tools + - - ; } \ No newline at end of file diff --git a/src/common/app.routes.ts b/src/common/app.routes.ts index 8d0ecd69e9..d8c8bff203 100644 --- a/src/common/app.routes.ts +++ b/src/common/app.routes.ts @@ -8,7 +8,6 @@ import Router from 'next/router'; export const ROUTE_APP_CHAT = '/'; const APP_LINK_CHAT = '/link/chat/:linkId'; -const APP_LABS = '/labs'; export const getHomeLink = () => ROUTE_APP_CHAT; @@ -16,8 +15,6 @@ export const getChatLinkRelativePath = (chatLinkId: string) => APP_LINK_CHAT.rep export const navigateToChat = async () => await Router.push(ROUTE_APP_CHAT); -export const navigateToLabs = async () => await Router.push(APP_LABS); - export const navigateBack = Router.back; export interface AppCallQueryParams { diff --git a/src/common/components/forms/FormSwitchControl.tsx b/src/common/components/forms/FormSwitchControl.tsx index 51056738fa..f369b885bd 100644 --- a/src/common/components/forms/FormSwitchControl.tsx +++ b/src/common/components/forms/FormSwitchControl.tsx @@ -10,16 +10,18 @@ import { FormLabelStart } from './FormLabelStart'; */ export function FormSwitchControl(props: { title: string | React.JSX.Element, description?: string | React.JSX.Element, - value: boolean, onChange: (on: boolean) => void, + on?: string, off?: string, fullWidth?: boolean, + checked: boolean, onChange: (on: boolean) => void, }) { return ( props.onChange(event.target.checked)} - endDecorator={props.value ? 'Enabled' : 'Off'} - sx={{ flexGrow: 1 }} + endDecorator={props.checked ? props.on || 'On' : props.off || 'Off'} + sx={props.fullWidth ? { flexGrow: 1 } : undefined} + slotProps={{ endDecorator: { sx: { minWidth: 26 } } }} /> ); diff --git a/src/common/components/forms/useFormRadio.tsx b/src/common/components/forms/useFormRadio.tsx index 1fa857224a..b6680d8d30 100644 --- a/src/common/components/forms/useFormRadio.tsx +++ b/src/common/components/forms/useFormRadio.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { FormControl, FormLabel, Radio, RadioGroup } from '@mui/joy'; -export type FormRadioOption = { label: string, value: T, experimental?: boolean }; +export type FormRadioOption = { label: string, value: T, disabled?: boolean }; /** @@ -14,9 +14,6 @@ export function useFormRadio(initialValue: T, options: FormRad // state const [value, setValue] = React.useState(initialValue); - // external state - // const experimentalLabs = useUIPreferencesStore(state => state.experimentalLabs); - const handleChange = React.useCallback((event: React.ChangeEvent) => { setValue(event.target.value as T | null); }, []); @@ -31,10 +28,10 @@ export function useFormRadio(initialValue: T, options: FormRad value={value} onChange={handleChange} > {options.map((option) => - )} + )} , - [/*experimentalLabs,*/ handleChange, hidden, label, options, value], + [handleChange, hidden, label, options, value], ); return [value, component]; diff --git a/src/common/state/store-ui.ts b/src/common/state/store-ui.ts index 702594f381..f15d4092e6 100644 --- a/src/common/state/store-ui.ts +++ b/src/common/state/store-ui.ts @@ -1,45 +1,13 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -// UI Counters - -interface UICountersStore { - actionCounters: Record; - incrementActionCounter: (key: string) => void; - clearActionCounter: (key: string) => void; - clearAllActionCounters: () => void; -} - -const useUICountersStore = create()( - persist( - (set) => ({ - actionCounters: {}, - incrementActionCounter: (key: string) => - set((state) => ({ - actionCounters: { ...state.actionCounters, [key]: (state.actionCounters[key] || 0) + 1 }, - })), - clearActionCounter: (key: string) => - set((state) => ({ - actionCounters: { ...state.actionCounters, [key]: 0 }, - })), - clearAllActionCounters: () => set({ actionCounters: {} }), - }), - { - name: 'app-ui-counters', - }, - ), -); - -type UiCounterKey = 'export-share' | 'share-chat-link' | 'call-wizard'; - -export function useUICounter(key: UiCounterKey) { - const value = useUICountersStore((state) => state.actionCounters[key] || 0); - return { value, novel: !value, touch: () => useUICountersStore.getState().incrementActionCounter(key) }; -} // UI Preferences interface UIPreferencesStore { + + // UI Features + preferredLanguage: string; setPreferredLanguage: (preferredLanguage: string) => void; @@ -52,9 +20,6 @@ interface UIPreferencesStore { enterIsNewline: boolean; setEnterIsNewline: (enterIsNewline: boolean) => void; - experimentalLabs: boolean; - setExperimentalLabs: (experimentalLabs: boolean) => void; - renderMarkdown: boolean; setRenderMarkdown: (renderMarkdown: boolean) => void; @@ -64,12 +29,19 @@ interface UIPreferencesStore { zenMode: 'clean' | 'cleaner'; setZenMode: (zenMode: 'clean' | 'cleaner') => void; + // UI Counters + + actionCounters: Record; + incrementActionCounter: (key: string) => void; + } export const useUIPreferencesStore = create()( persist( (set) => ({ + // UI Features + preferredLanguage: (typeof navigator !== 'undefined') && navigator.language || 'en-US', setPreferredLanguage: (preferredLanguage: string) => set({ preferredLanguage }), @@ -82,9 +54,6 @@ export const useUIPreferencesStore = create()( enterIsNewline: false, setEnterIsNewline: (enterIsNewline: boolean) => set({ enterIsNewline }), - experimentalLabs: false, - setExperimentalLabs: (experimentalLabs: boolean) => set({ experimentalLabs }), - renderMarkdown: true, setRenderMarkdown: (renderMarkdown: boolean) => set({ renderMarkdown }), @@ -95,6 +64,14 @@ export const useUIPreferencesStore = create()( zenMode: 'clean', setZenMode: (zenMode: 'clean' | 'cleaner') => set({ zenMode }), + // UI Counters + + actionCounters: {}, + incrementActionCounter: (key: string) => + set((state) => ({ + actionCounters: { ...state.actionCounters, [key]: (state.actionCounters[key] || 0) + 1 }, + })), + }), { name: 'app-ui', @@ -113,3 +90,12 @@ export const useUIPreferencesStore = create()( }, ), ); + +export function useUICounter(key: 'export-share' | 'share-chat-link' | 'call-wizard') { + const value = useUIPreferencesStore((state) => state.actionCounters[key] || 0); + return { + value, + novel: !value, + touch: () => useUIPreferencesStore.getState().incrementActionCounter(key), + }; +} \ No newline at end of file diff --git a/src/common/state/store-ux-labs.ts b/src/common/state/store-ux-labs.ts new file mode 100644 index 0000000000..8b6e8a0950 --- /dev/null +++ b/src/common/state/store-ux-labs.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + + +// UX Labs Experiments + +/** + * Graduated: + * - Persona YT Creator: still under a 'true' flag, to disable it if needed + * - Text Tools: dinamically shown where applicable + * - Chat Mode: follow-ups; moved to Chat Advanced UI, itemized (Auto-title, Auto-diagram) + */ +interface UXLabsStore { + + labsEnhancedUI: boolean; + setLabsEnhancedUI: (labsEnhancedUI: boolean) => void; + + labsMagicDraw: boolean; + setLabsMagicDraw: (labsMagicDraw: boolean) => void; + + labsPersonaYTCreator: boolean; + setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => void; + +} + +export const useUXLabsStore = create()( + persist( + (set) => ({ + + labsEnhancedUI: false, + setLabsEnhancedUI: (labsEnhancedUI: boolean) => set({ labsEnhancedUI }), + + labsMagicDraw: false, + setLabsMagicDraw: (labsMagicDraw: boolean) => set({ labsMagicDraw }), + + labsPersonaYTCreator: true, // NOTE: default to true, as it is a graduated experiment + setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => set({ labsPersonaYTCreator }), + + }), + { + name: 'app-ux-labs', + }, + ), +); \ No newline at end of file diff --git a/src/modules/llms/vendors/openai/OpenAISourceSetup.tsx b/src/modules/llms/vendors/openai/OpenAISourceSetup.tsx index e23d93dcf4..2a0ad05457 100644 --- a/src/modules/llms/vendors/openai/OpenAISourceSetup.tsx +++ b/src/modules/llms/vendors/openai/OpenAISourceSetup.tsx @@ -95,12 +95,12 @@ export function OpenAISourceSetup(props: { sourceId: DModelSourceId }) { } {advanced.on && Overview, {' '}policy } - value={moderationCheck} + checked={moderationCheck} onChange={on => updateSetup({ moderationCheck: on })} />} From 6ed8529d6af4bc9c9c39528d9da184a2802bd302 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 21 Nov 2023 22:06:18 -0800 Subject: [PATCH 02/44] Roll types --- package-lock.json | 28 ++++++++++++++-------------- package.json | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 111baa7769..74e92857d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,11 +46,11 @@ "zustand": "~4.3.9" }, "devDependencies": { - "@types/node": "^20.9.3", + "@types/node": "^20.9.4", "@types/plantuml-encoder": "^1.4.2", "@types/prismjs": "^1.26.3", "@types/react": "^18.2.38", - "@types/react-dom": "^18.2.16", + "@types/react-dom": "^18.2.17", "@types/react-katex": "^3.0.3", "@types/react-timeago": "^4.1.6", "@types/uuid": "^9.0.7", @@ -1102,9 +1102,9 @@ "integrity": "sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.1.tgz", - "integrity": "sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz", + "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==", "dev": true }, "node_modules/@sanity/diff-match-patch": { @@ -1282,9 +1282,9 @@ "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/node": { - "version": "20.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", - "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", + "version": "20.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", + "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1326,9 +1326,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.16", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.16.tgz", - "integrity": "sha512-766c37araZ9vxtYs25gvY2wNdFWsT2ZiUvOd0zMhTaoGj6B911N8CKQWgXXJoPMLF3J82thpRqQA7Rf3rBwyJw==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "dev": true, "dependencies": { "@types/react": "*" @@ -1362,9 +1362,9 @@ } }, "node_modules/@types/scheduler": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.7.tgz", - "integrity": "sha512-8g25Nl3AuB1KulTlSUsUhUo/oBgBU6XIXQ+XURpeioEbEJvkO7qI4vDfREv3vJYHHzqXjcAHvoJy4pTtSQNZtA==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/unist": { "version": "3.0.2", diff --git a/package.json b/package.json index 12af8663d4..8f95595b30 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,11 @@ "zustand": "~4.3.9" }, "devDependencies": { - "@types/node": "^20.9.3", + "@types/node": "^20.9.4", "@types/plantuml-encoder": "^1.4.2", "@types/prismjs": "^1.26.3", "@types/react": "^18.2.38", - "@types/react-dom": "^18.2.16", + "@types/react-dom": "^18.2.17", "@types/react-katex": "^3.0.3", "@types/react-timeago": "^4.1.6", "@types/uuid": "^9.0.7", From 669eb1414f7449033eafb751d7b1215d524bb34b Mon Sep 17 00:00:00 2001 From: g1ibby Date: Wed, 22 Nov 2023 13:14:46 +0700 Subject: [PATCH 03/44] fix: ollama listModel endpoint when a model doesn't have TEMPLATE or PARAMETER --- src/modules/llms/transports/server/ollama/ollama.router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/llms/transports/server/ollama/ollama.router.ts b/src/modules/llms/transports/server/ollama/ollama.router.ts index b450dc2069..ecb3673108 100644 --- a/src/modules/llms/transports/server/ollama/ollama.router.ts +++ b/src/modules/llms/transports/server/ollama/ollama.router.ts @@ -182,8 +182,8 @@ export const llmOllamaRouter = createTRPCRouter({ const wireOllamaModelInfoSchema = z.object({ license: z.string().optional(), modelfile: z.string(), - parameters: z.string(), - template: z.string(), + parameters: z.string().optional(), + template: z.string().optional(), }); const modelInfo = wireOllamaModelInfoSchema.parse(wireModelInfo); return { ...model, ...modelInfo }; From 16916db24761c5fa583dfd749a9323712e7b7684 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Tue, 21 Nov 2023 22:17:17 -0800 Subject: [PATCH 04/44] Improve routing, and move the action pwa action receiver --- pages/{launch.tsx => link/share_target.tsx} | 13 +++++++------ public/manifest.json | 2 +- src/apps/chat/components/composer/Composer.tsx | 2 +- src/apps/link/AppChatLinkDrawerItems.tsx | 4 ++-- src/apps/news/AppNews.tsx | 4 ++-- src/common/app.routes.ts | 15 +++++++++++---- 6 files changed, 24 insertions(+), 16 deletions(-) rename pages/{launch.tsx => link/share_target.tsx} (91%) diff --git a/pages/launch.tsx b/pages/link/share_target.tsx similarity index 91% rename from pages/launch.tsx rename to pages/link/share_target.tsx index a25fbf174f..6f05c4000c 100644 --- a/pages/launch.tsx +++ b/pages/link/share_target.tsx @@ -4,11 +4,12 @@ import { useRouter } from 'next/router'; import { Alert, Box, Button, Typography } from '@mui/joy'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { setComposerStartupText } from '../src/apps/chat/components/composer/store-composer'; +import { setComposerStartupText } from '../../src/apps/chat/components/composer/store-composer'; import { AppLayout } from '~/common/layout/AppLayout'; import { LogoProgress } from '~/common/components/LogoProgress'; import { asValidURL } from '~/common/util/urlUtils'; +import { navigateToIndex } from '~/common/app.routes'; /** @@ -28,13 +29,13 @@ function AppShareTarget() { const [isDownloading, setIsDownloading] = React.useState(false); // external state - const { query, push: routerPush, replace: routerReplace } = useRouter(); + const { query } = useRouter(); const queueComposerTextAndLaunchApp = React.useCallback((text: string) => { setComposerStartupText(text); - void routerReplace('/'); - }, [routerReplace]); + void navigateToIndex(true); + }, []); // Detect the share Intent from the query @@ -110,7 +111,7 @@ function AppShareTarget() { )} @@ -199,8 +195,6 @@ export function SettingsModal() { - {showShortcuts && setShowShortcuts(false)} />} - ); } diff --git a/src/apps/settings-modal/ShortcutsModal.tsx b/src/apps/settings-modal/ShortcutsModal.tsx index dc76b83293..181b667458 100644 --- a/src/apps/settings-modal/ShortcutsModal.tsx +++ b/src/apps/settings-modal/ShortcutsModal.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { ChatMessage } from '../chat/components/message/ChatMessage'; import { GoodModal } from '~/common/components/GoodModal'; +import { closeLayoutShortcuts, useLayoutShortcuts } from '~/common/layout/store-applayout'; import { createDMessage } from '~/common/state/store-chats'; import { platformAwareKeystrokes } from '~/common/components/KeyStroke'; @@ -12,8 +13,8 @@ const shortcutsMd = ` | Shortcut | Description | |---------------------|-------------------------------------------------| | **Edit** | | -| Shift + Enter | Newline (don't send) | -| Alt + Enter | Append message (don't send) | +| Shift + Enter | Newline | +| Alt + Enter | Append (no response) | | Ctrl + Shift + R | Regenerate answer | | Ctrl + Shift + V | Attach clipboard (better than Ctrl + V) | | Ctrl + M | Microphone (voice typing) | @@ -27,16 +28,25 @@ const shortcutsMd = ` | **Settings** | | | Ctrl + Shift + M | 🧠 Models | | Ctrl + Shift + P | ⚙️ Preferences | +| Ctrl + Shift + ? | Shortcuts | `.trim(); const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd)); -export const ShortcutsModal = (props: { onClose: () => void }) => - - - ; \ No newline at end of file +export function ShortcutsModal() { + + // external state + const showShortcuts = useLayoutShortcuts(); + + return ( + + + + ); +} \ No newline at end of file diff --git a/src/common/layout/AppLayout.tsx b/src/common/layout/AppLayout.tsx index b1384be42e..46e52373b1 100644 --- a/src/common/layout/AppLayout.tsx +++ b/src/common/layout/AppLayout.tsx @@ -5,6 +5,7 @@ import { Box, Container } from '@mui/joy'; import { ModelsModal } from '../../apps/models-modal/ModelsModal'; import { SettingsModal } from '../../apps/settings-modal/SettingsModal'; +import { ShortcutsModal } from '../../apps/settings-modal/ShortcutsModal'; import { isPwa } from '~/common/util/pwaUtils'; import { useAppStateStore } from '~/common/state/store-appstate'; @@ -13,7 +14,7 @@ import { useUIPreferencesStore } from '~/common/state/store-ui'; import { AppBar } from './AppBar'; import { GlobalShortcutItem, useGlobalShortcuts } from '../components/useGlobalShortcut'; import { NoSSR } from '../components/NoSSR'; -import { openLayoutModelsSetup, openLayoutPreferences } from './store-applayout'; +import { openLayoutModelsSetup, openLayoutPreferences, openLayoutShortcuts } from './store-applayout'; export function AppLayout(props: { @@ -30,6 +31,7 @@ export function AppLayout(props: { const shortcuts = React.useMemo((): GlobalShortcutItem[] => [ ['m', true, true, false, openLayoutModelsSetup], ['p', true, true, false, openLayoutPreferences], + ['?', true, true, false, openLayoutShortcuts], ], []); useGlobalShortcuts(shortcuts); @@ -69,6 +71,9 @@ export function AppLayout(props: { {/* Overlay Models (& Model Options )*/} + {/* Overlay Shortcuts */} + + ); } \ No newline at end of file diff --git a/src/common/layout/store-applayout.ts b/src/common/layout/store-applayout.ts index 9c1e37e5d1..2bd211966c 100644 --- a/src/common/layout/store-applayout.ts +++ b/src/common/layout/store-applayout.ts @@ -19,6 +19,7 @@ interface AppLayoutStore { preferencesTab: number; // 0: closed, 1..N: tab index modelsSetupOpen: boolean; llmOptionsId: DLLMId | null; + shortcutsOpen: boolean; } @@ -35,6 +36,7 @@ const useAppLayoutStore = create()( preferencesTab: 0, modelsSetupOpen: false, llmOptionsId: null, + shortcutsOpen: false, }), ); @@ -74,4 +76,7 @@ export const useLayoutModelsSetup = (): [open: boolean, llmId: DLLMId | null] => export const openLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: true }); export const closeLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: false }); export const openLayoutLLMOptions = (llmId: DLLMId) => useAppLayoutStore.setState({ llmOptionsId: llmId }); -export const closeLayoutLLMOptions = () => useAppLayoutStore.setState({ llmOptionsId: null }); \ No newline at end of file +export const closeLayoutLLMOptions = () => useAppLayoutStore.setState({ llmOptionsId: null }); +export const useLayoutShortcuts = () => useAppLayoutStore(state => state.shortcutsOpen); +export const openLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: true }); +export const closeLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: false }); \ No newline at end of file From 76f21f8c960ebce5c93c6f8f6ac716257ba48e56 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 02:22:20 -0800 Subject: [PATCH 16/44] Rename --- src/apps/chat/components/applayout/ChatDrawerItems.tsx | 4 ++-- .../{ConversationItem.tsx => ConversationNavigationItem.tsx} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/apps/chat/components/applayout/{ConversationItem.tsx => ConversationNavigationItem.tsx} (98%) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index 0b5b7f34e5..c5ed47c0b2 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -12,7 +12,7 @@ import { closeLayoutDrawer } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; -import { ConversationItem } from './ConversationItem'; +import { ConversationNavigationItem } from './ConversationNavigationItem'; type ListGrouping = 'off' | 'persona'; @@ -116,7 +116,7 @@ export function ChatDrawerItems(props: { {/**/} {sortedIds.map(conversationId => - - {/* Optional prgoress bar */} + {/* Optional progress bar, underlay */} {progress > 0 && ( Date: Wed, 22 Nov 2023 02:32:24 -0800 Subject: [PATCH 17/44] Rename --- .../components/applayout/ChatDrawerItems.tsx | 4 ++-- ...igationItem.tsx => ChatNavigationItem.tsx} | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) rename src/apps/chat/components/applayout/{ConversationNavigationItem.tsx => ChatNavigationItem.tsx} (92%) diff --git a/src/apps/chat/components/applayout/ChatDrawerItems.tsx b/src/apps/chat/components/applayout/ChatDrawerItems.tsx index c5ed47c0b2..a7c3e639b7 100644 --- a/src/apps/chat/components/applayout/ChatDrawerItems.tsx +++ b/src/apps/chat/components/applayout/ChatDrawerItems.tsx @@ -12,7 +12,7 @@ import { closeLayoutDrawer } from '~/common/layout/store-applayout'; import { useUIPreferencesStore } from '~/common/state/store-ui'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; -import { ConversationNavigationItem } from './ConversationNavigationItem'; +import { ChatNavigationItem } from './ChatNavigationItem'; type ListGrouping = 'off' | 'persona'; @@ -116,7 +116,7 @@ export function ChatDrawerItems(props: { {/**/} {sortedIds.map(conversationId => - void, }) { + const { isActive } = props; + // state const [isEditingTitle, setIsEditingTitle] = React.useState(false); const [deleteArmed, setDeleteArmed] = React.useState(false); @@ -50,9 +52,9 @@ export function ConversationNavigationItem(props: { // NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening // because the isActive prop is not yet updated React.useEffect(() => { - if (deleteArmed && !props.isActive) + if (deleteArmed && !isActive) setDeleteArmed(false); - }, [deleteArmed, props.isActive]); + }, [deleteArmed, isActive]); // sanity check: shouldn't happen, but just in case if (!cState) return null; @@ -70,7 +72,7 @@ export function ConversationNavigationItem(props: { const handleDeleteButtonShow = (event: React.MouseEvent) => { event.stopPropagation(); - if (!props.isActive) + if (!isActive) props.onConversationActivate(props.conversationId, false); else setDeleteArmed(true); @@ -88,20 +90,21 @@ export function ConversationNavigationItem(props: { const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓'; - const buttonSx: SxProps = { ml: 1, ...(props.isActive ? { color: 'white' } : {}) }; + const buttonSx: SxProps = { ml: 1, ...(isActive ? { color: 'white' } : {}) }; const progress = props.maxChatMessages ? 100 * messageCount / props.maxChatMessages : 0; return ( button': { opacity: 1 }, + ...(isActive ? { bgcolor: 'red' } : {}), }} > @@ -160,7 +163,7 @@ export function ConversationNavigationItem(props: { {/* Delete Arming */} {!props.isLonely && !deleteArmed && ( From e55fbe9ad01655f0d284c5445425b211a116c64a Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 03:14:16 -0800 Subject: [PATCH 18/44] Fix missing hook dep --- src/apps/chat/AppChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 983aee2725..5c5626bc0d 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -303,7 +303,7 @@ export function AppChat() { onConversationNew={handleConversationNew} onConversationsDeleteAll={handleConversationsDeleteAll} />, - [focusedConversationId, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId], + [focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId], ); const menuItems = React.useMemo(() => From ce13c04e96efe745a116752abdd53c4a0be3553a Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 04:00:28 -0800 Subject: [PATCH 19/44] Perf Boost - large gains on the Nav Drawer --- src/apps/chat/AppChat.tsx | 6 +-- .../components/applayout/ChatDrawerItems.tsx | 46 +++++++------------ .../applayout/ChatNavigationItem.tsx | 45 ++++++++---------- 3 files changed, 39 insertions(+), 58 deletions(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 5c5626bc0d..2bfccbfb06 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -19,7 +19,7 @@ import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/commo import { useLayoutPluggable } from '~/common/layout/store-applayout'; import { useUXLabsStore } from '~/common/state/store-ux-labs'; -import { ChatDrawerItems } from './components/applayout/ChatDrawerItems'; +import { ChatDrawerItemsMemo } from './components/applayout/ChatDrawerItems'; import { ChatDropdowns } from './components/applayout/ChatDropdowns'; import { ChatMenuItems } from './components/applayout/ChatMenuItems'; import { ChatMessageList } from './components/ChatMessageList'; @@ -294,8 +294,8 @@ export function AppChat() { ); const drawerItems = React.useMemo(() => - void, onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void, @@ -29,18 +31,16 @@ export function ChatDrawerItems(props: { // local state const { onConversationDelete, onConversationNew, onConversationActivate } = props; - const [grouping] = React.useState('off'); + // const [grouping] = React.useState('off'); // external state - const { conversationIDs, maxChatMessages } = useChatStore(state => ({ - conversationIDs: state.conversations.map(_c => _c.id), - maxChatMessages: state.conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 0), - }), shallow); + const conversations = useChatStore(state => state.conversations, shallow); const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner'); const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI); // derived state - const totalConversations = conversationIDs.length; + const maxChatMessages = conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1); + const totalConversations = conversations.length; const hasChats = totalConversations > 0; const singleChat = totalConversations === 1; const softMaxReached = totalConversations >= 50; @@ -63,7 +63,7 @@ export function ChatDrawerItems(props: { // grouping - let sortedIds = conversationIDs; + /*let sortedIds = conversationIDs; if (grouping === 'persona') { const conversations = useChatStore.getState().conversations; @@ -80,7 +80,7 @@ export function ChatDrawerItems(props: { // flatten grouped conversations sortedIds = Object.values(groupedConversations).flat(); - } + }*/ return <> @@ -115,11 +115,11 @@ export function ChatDrawerItems(props: { {/* */} {/**/} - {sortedIds.map(conversationId => - + - {/**/} - {/* */} - {/* Scratchpad*/} - {/* */} - {/**/} - {/**/} - {/* */} - {/* */} - {/* Feature #17*/} - {/* */} - {/**/} - ; } \ No newline at end of file diff --git a/src/apps/chat/components/applayout/ChatNavigationItem.tsx b/src/apps/chat/components/applayout/ChatNavigationItem.tsx index 54674627a9..9dec2a3b7b 100644 --- a/src/apps/chat/components/applayout/ChatNavigationItem.tsx +++ b/src/apps/chat/components/applayout/ChatNavigationItem.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { shallow } from 'zustand/shallow'; import { Avatar, Box, IconButton, ListItemDecorator, MenuItem, Typography } from '@mui/joy'; import { SxProps } from '@mui/joy/styles/types'; @@ -9,15 +8,17 @@ import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import { SystemPurposes } from '../../../../data'; import { InlineTextarea } from '~/common/components/InlineTextarea'; -import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats'; +import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats'; import { useUIPreferencesStore } from '~/common/state/store-ui'; const DEBUG_CONVERSATION_IDs = false; -export function ChatNavigationItem(props: { - conversationId: DConversationId, +export const ChatNavigationItemMemo = React.memo(ChatNavigationItem); + +function ChatNavigationItem(props: { + conversation: DConversation, isActive: boolean, isLonely: boolean, maxChatMessages: number, @@ -26,7 +27,7 @@ export function ChatNavigationItem(props: { onConversationDelete: (conversationId: DConversationId) => void, }) { - const { isActive } = props; + const { conversation, isActive } = props; // state const [isEditingTitle, setIsEditingTitle] = React.useState(false); @@ -35,18 +36,14 @@ export function ChatNavigationItem(props: { // external state const doubleClickToEdit = useUIPreferencesStore(state => state.doubleClickToEdit); - // bind to conversation - const cState = useChatStore(state => { - const conversation = state.conversations.find(conversation => conversation.id === props.conversationId); - return conversation && { - isNew: conversation.messages.length === 0, - messageCount: conversation.messages.length, - assistantTyping: !!conversation.abortController, - systemPurposeId: conversation.systemPurposeId, - title: conversationTitle(conversation, 'new conversation'), - setUserTitle: state.setUserTitle, - }; - }, shallow); + // derived state + const { id: conversationId } = conversation; + const isNew = conversation.messages.length === 0; + const messageCount = conversation.messages.length; + const assistantTyping = !!conversation.abortController; + const systemPurposeId = conversation.systemPurposeId; + const title = conversationTitle(conversation, 'new conversation'); + // const setUserTitle = state.setUserTitle; // auto-close the arming menu when clicking away // NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening @@ -56,24 +53,20 @@ export function ChatNavigationItem(props: { setDeleteArmed(false); }, [deleteArmed, isActive]); - // sanity check: shouldn't happen, but just in case - if (!cState) return null; - const { isNew, messageCount, assistantTyping, setUserTitle, systemPurposeId, title } = cState; - - const handleConversationActivate = () => props.onConversationActivate(props.conversationId, true); + const handleConversationActivate = () => props.onConversationActivate(conversationId, true); const handleTitleEdit = () => setIsEditingTitle(true); const handleTitleEdited = (text: string) => { setIsEditingTitle(false); - setUserTitle(props.conversationId, text); + useChatStore.getState().setUserTitle(conversationId, text); }; const handleDeleteButtonShow = (event: React.MouseEvent) => { event.stopPropagation(); if (!isActive) - props.onConversationActivate(props.conversationId, false); + props.onConversationActivate(conversationId, false); else setDeleteArmed(true); }; @@ -84,7 +77,7 @@ export function ChatNavigationItem(props: { if (deleteArmed) { setDeleteArmed(false); event.stopPropagation(); - props.onConversationDelete(props.conversationId); + props.onConversationDelete(conversationId); } }; @@ -140,7 +133,7 @@ export function ChatNavigationItem(props: { {!isEditingTitle ? ( doubleClickToEdit ? handleTitleEdit() : null} sx={{ flexGrow: 1 }}> - {DEBUG_CONVERSATION_IDs ? props.conversationId.slice(0, 10) : title}{assistantTyping && '...'} + {DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : title}{assistantTyping && '...'} ) : ( From d23f247a8c4d88c42e8c0f7610b8281986597c91 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 04:06:27 -0800 Subject: [PATCH 20/44] Large Perf Boost on Messages --- src/apps/chat/components/ChatMessageList.tsx | 101 +++++++++--------- .../chat/components/message/ChatMessage.tsx | 2 + 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/src/apps/chat/components/ChatMessageList.tsx b/src/apps/chat/components/ChatMessageList.tsx index 2d815b5a33..1931d1f61e 100644 --- a/src/apps/chat/components/ChatMessageList.tsx +++ b/src/apps/chat/components/ChatMessageList.tsx @@ -9,11 +9,11 @@ import { useChatLLM } from '~/modules/llms/store-llms'; import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut'; import { InlineError } from '~/common/components/InlineError'; -import { createDMessage, DConversationId, DMessage, useChatStore } from '~/common/state/store-chats'; +import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats'; import { openLayoutPreferences } from '~/common/layout/store-applayout'; import { useCapabilityElevenLabs, useCapabilityProdia } from '~/common/components/useCapabilities'; -import { ChatMessage } from './message/ChatMessage'; +import { ChatMessageMemo } from './message/ChatMessage'; import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage'; import { PersonaSelector } from './persona-selector/PersonaSelector'; import { useChatShowSystemMessages } from '../store-app-chat'; @@ -40,10 +40,10 @@ export function ChatMessageList(props: { // external state const [showSystemMessages] = useChatShowSystemMessages(); - const { messages, editMessage, deleteMessage, historyTokenCount } = useChatStore(state => { + const { conversationMessages, editMessage, deleteMessage, historyTokenCount } = useChatStore(state => { const conversation = state.conversations.find(conversation => conversation.id === props.conversationId); return { - messages: conversation ? conversation.messages : [], + conversationMessages: conversation ? conversation.messages : [], editMessage: state.editMessage, deleteMessage: state.deleteMessage, historyTokenCount: conversation ? conversation.tokenCount : 0, }; @@ -52,57 +52,59 @@ export function ChatMessageList(props: { const { mayWork: isImaginable } = useCapabilityProdia(); const { mayWork: isSpeakable } = useCapabilityElevenLabs(); + // derived state + const { conversationId, onConversationExecuteHistory, onConversationBranch, onTextDiagram, onTextImagine, onTextSpeak } = props; + // text actions const handleRunExample = (text: string) => - props.conversationId && props.onConversationExecuteHistory(props.conversationId, [...messages, createDMessage('user', text)]); + conversationId && onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', text)]); // message menu methods proxy - const handleConversationBranch = (messageId: string) => { - props.conversationId && props.onConversationBranch(props.conversationId, messageId); - }; + const handleConversationBranch = React.useCallback((messageId: string) => { + conversationId && onConversationBranch(conversationId, messageId); + }, [conversationId, onConversationBranch]); - const handleConversationRestartFrom = (messageId: string, offset: number) => { - const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + offset + 1); - props.conversationId && props.onConversationExecuteHistory(props.conversationId, truncatedHistory); - }; + const handleConversationRestartFrom = React.useCallback((messageId: string, offset: number) => { + const messages = getConversation(conversationId)?.messages; + if (messages) { + const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + offset + 1); + conversationId && onConversationExecuteHistory(conversationId, truncatedHistory); + } + }, [conversationId, onConversationExecuteHistory]); - const handleMessageDelete = (messageId: string) => - props.conversationId && deleteMessage(props.conversationId, messageId); + const handleMessageDelete = React.useCallback((messageId: string) => { + conversationId && deleteMessage(conversationId, messageId); + }, [conversationId, deleteMessage]); - const handleMessageEdit = (messageId: string, newText: string) => - props.conversationId && editMessage(props.conversationId, messageId, { text: newText }, true); + const handleMessageEdit = React.useCallback((messageId: string, newText: string) => { + conversationId && editMessage(conversationId, messageId, { text: newText }, true); + }, [conversationId, editMessage]); - const handleTextDiagram = async (messageId: string, text: string) => { - if (props.conversationId) { - await props.onTextDiagram({ conversationId: props.conversationId, messageId, text }); - } else - return Promise.reject('No conversation'); - }; + const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => { + conversationId && await onTextDiagram({ conversationId: conversationId, messageId, text }); + }, [conversationId, onTextDiagram]); - const handleTextImagine = async (text: string) => { - if (!isImaginable) { - openLayoutPreferences(2); - } else if (props.conversationId) { + const handleTextImagine = React.useCallback(async (text: string) => { + if (!isImaginable) + return openLayoutPreferences(2); + if (conversationId) { setIsImagining(true); - await props.onTextImagine(props.conversationId, text); + await onTextImagine(conversationId, text); setIsImagining(false); - } else - return Promise.reject('No conversation'); - }; - - const handleTextSpeak = async (text: string) => { - if (!isSpeakable) { - openLayoutPreferences(3); - } else { - setIsSpeaking(true); - await props.onTextSpeak(text); - setIsSpeaking(false); } - }; + }, [conversationId, isImaginable, onTextImagine]); + + const handleTextSpeak = React.useCallback(async (text: string) => { + if (!isSpeakable) + return openLayoutPreferences(3); + setIsSpeaking(true); + await onTextSpeak(text); + setIsSpeaking(false); + }, [isSpeakable, onTextSpeak]); // operate on the local selection set @@ -110,7 +112,7 @@ export function ChatMessageList(props: { const handleSelectAll = (selected: boolean) => { const newSelected = new Set(); if (selected) - for (const message of messages) + for (const message of conversationMessages) newSelected.add(message.id); setSelectedMessages(newSelected); }; @@ -122,9 +124,9 @@ export function ChatMessageList(props: { }; const handleSelectionDelete = () => { - if (props.conversationId) + if (conversationId) for (const selectedMessage of selectedMessages) - deleteMessage(props.conversationId, selectedMessage); + deleteMessage(conversationId, selectedMessage); setSelectedMessages(new Set()); }; @@ -136,7 +138,7 @@ export function ChatMessageList(props: { // text-diff functionality, find the messages to diff with const { diffMessage, diffText } = React.useMemo(() => { - const [msgB, msgA] = messages.filter(m => m.role === 'assistant').reverse(); + const [msgB, msgA] = conversationMessages.filter(m => m.role === 'assistant').reverse(); if (msgB?.text && msgA?.text && !msgB?.typing) { const textA = msgA.text, textB = msgB.text; const lenA = textA.length, lenB = textB.length; @@ -144,21 +146,20 @@ export function ChatMessageList(props: { return { diffMessage: msgB, diffText: textA }; } return { diffMessage: undefined, diffText: undefined }; - }, [messages]); + }, [conversationMessages]); // no content: show the persona selector - const filteredMessages = messages + const filteredMessages = conversationMessages .filter(m => m.role !== 'system' || showSystemMessages) // hide the System message if the user choses to .reverse(); // 'reverse' is because flexDirection: 'column-reverse' to auto-snap-to-bottom - // when there are no messages, show the purpose selector if (!filteredMessages.length) return ( - {props.conversationId - ? - : } + {conversationId + ? + : } ); @@ -183,7 +184,7 @@ export function ChatMessageList(props: { ) : ( - Date: Wed, 22 Nov 2023 04:32:19 -0800 Subject: [PATCH 21/44] Title: show the chat index (1: first, 2: second most recently created, etc) --- src/apps/chat/AppChat.tsx | 7 +++++-- src/common/state/store-chats.ts | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 2bfccbfb06..c0a61d5160 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -62,8 +62,10 @@ export function AppChat() { openConversationInSplitPane, setFocusedPaneIndex, } = usePanesManager(); + const { title: focusedChatTitle, + chatIdx: focusedChatNumber, isChatEmpty: isFocusedChatEmpty, areChatsEmpty, newConversationId, @@ -100,10 +102,11 @@ export function AppChat() { React.useEffect(() => { if (showNextTitle.current) { showNextTitle.current = false; - const id = addSnackbar({ key: 'focused-title', message: focusedChatTitle || 'New Chat', type: 'title' }); + const title = (focusedChatNumber >= 0 ? `#${focusedChatNumber + 1} · ` : '') + (focusedChatTitle || 'New Chat'); + const id = addSnackbar({ key: 'focused-title', message: title, type: 'title' }); return () => removeSnackbar(id); } - }, [focusedChatTitle]); + }, [focusedChatNumber, focusedChatTitle]); // Execution diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index 231bf60cb9..5f6c50cb83 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -543,12 +543,14 @@ export const useConversation = (conversationId: DConversationId | null) => useCh // this object will change if any sub-prop changes as well const conversation = conversationId ? conversations.find(_c => _c.id === conversationId) ?? null : null; const title = conversation ? conversationTitle(conversation) : null; + const chatIdx = conversation ? conversations.findIndex(_c => _c.id === conversation.id) : -1; const isChatEmpty = conversation ? !conversation.messages.length : true; const areChatsEmpty = isChatEmpty && conversations.length < 2; const newConversationId: DConversationId | null = (conversations.length && !conversations[0].messages.length) ? conversations[0].id : null; return { title, + chatIdx, isChatEmpty, areChatsEmpty, newConversationId, From 5387b17c36ad1a14cf9bc1bc05dd550f34025a97 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 13:03:03 -0800 Subject: [PATCH 22/44] Also show the branched title. --- src/apps/chat/AppChat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index c0a61d5160..235dc7386e 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -225,6 +225,7 @@ export function AppChat() { const handleConversationExport = (conversationId: DConversationId | null) => setTradeConfig({ dir: 'export', conversationId }); const handleConversationBranch = React.useCallback((conversationId: DConversationId, messageId: string | null) => { + showNextTitle.current = true; const branchedConversationId = branchConversation(conversationId, messageId); addSnackbar({ key: 'branch-conversation', From 53b5da8cb8e737d3f8505b3622ff13968c1f7e09 Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Wed, 22 Nov 2023 22:32:45 -0800 Subject: [PATCH 23/44] OpenAI Shared Chats: import from Clipboard too, and copy json object --- src/modules/trade/ExportChats.tsx | 3 +- src/modules/trade/ExportedChatLink.tsx | 2 +- src/modules/trade/ExportedPublish.tsx | 2 +- src/modules/trade/ImportChats.tsx | 48 ++++++++++++++++++------ src/modules/trade/ImportOutcomeModal.tsx | 26 ++++++++++++- src/modules/trade/server/link.ts | 4 ++ src/modules/trade/server/pastegg.ts | 1 + src/modules/trade/server/trade.router.ts | 46 +++++++++++++---------- 8 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/modules/trade/ExportChats.tsx b/src/modules/trade/ExportChats.tsx index da3ce1cbf8..04fa3fd0ae 100644 --- a/src/modules/trade/ExportChats.tsx +++ b/src/modules/trade/ExportChats.tsx @@ -18,7 +18,8 @@ import { conversationTitle, DConversationId, getConversation } from '~/common/st import { isBrowser } from '~/common/util/pwaUtils'; import { useUICounter } from '~/common/state/store-ui'; -import type { PublishedSchema, StoragePutSchema } from './server/trade.router'; +import type { PublishedSchema } from './server/pastegg'; +import type { StoragePutSchema } from './server/link'; import { ExportedChatLink } from './ExportedChatLink'; import { ExportedPublish } from './ExportedPublish'; import { addChatLinkItem, useLinkStorageOwnerId } from './store-module-trade'; diff --git a/src/modules/trade/ExportedChatLink.tsx b/src/modules/trade/ExportedChatLink.tsx index 59622b04e1..22632f1e4c 100644 --- a/src/modules/trade/ExportedChatLink.tsx +++ b/src/modules/trade/ExportedChatLink.tsx @@ -19,7 +19,7 @@ import { getChatLinkRelativePath } from '~/common/app.routes'; import { getOriginUrl } from '~/common/util/urlUtils'; import { webShare, webSharePresent } from '~/common/util/pwaUtils'; -import type { StorageDeleteSchema, StoragePutSchema } from './server/trade.router'; +import type { StorageDeleteSchema, StoragePutSchema } from './server/link'; import { removeChatLinkItem } from './store-module-trade'; diff --git a/src/modules/trade/ExportedPublish.tsx b/src/modules/trade/ExportedPublish.tsx index c03685a9ce..2255b3b4fa 100644 --- a/src/modules/trade/ExportedPublish.tsx +++ b/src/modules/trade/ExportedPublish.tsx @@ -4,7 +4,7 @@ import { Alert, Box, Button, Divider, Input, Modal, ModalDialog, Stack, Typograp import { Link } from '~/common/components/Link'; -import type { PublishedSchema } from './server/trade.router'; +import type { PublishedSchema } from './server/pastegg'; /** diff --git a/src/modules/trade/ImportChats.tsx b/src/modules/trade/ImportChats.tsx index d81266f78e..0b859d39a6 100644 --- a/src/modules/trade/ImportChats.tsx +++ b/src/modules/trade/ImportChats.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { fileOpen, FileWithHandle } from 'browser-fs-access'; -import { Box, Button, FormControl, Input, Sheet, Typography } from '@mui/joy'; +import { Box, Button, FormControl, Input, Sheet, Textarea, Typography } from '@mui/joy'; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { Brand } from '~/common/app.config'; -import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; +import { FormRadioOption, useFormRadio } from '~/common/components/forms/useFormRadio'; import { InlineError } from '~/common/components/InlineError'; import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon'; import { apiAsyncNode } from '~/common/util/trpc.client'; @@ -19,6 +19,12 @@ import { ImportedOutcome, ImportOutcomeModal } from './ImportOutcomeModal'; export type ImportConfig = { dir: 'import' }; + +const chatGptMedia: FormRadioOption<'source' | 'link'>[] = [ + { label: 'Shared Chat URL', value: 'link' }, + { label: 'Page Source', value: 'source' }, +]; + /** * Components and functionality to import conversations * Supports our own JSON files, and ChatGPT Share Links @@ -26,13 +32,19 @@ export type ImportConfig = { dir: 'import' }; export function ImportConversations(props: { onClose: () => void }) { // state + const [importMedia, importMediaControl] = useFormRadio('link', chatGptMedia); const [chatGptEdit, setChatGptEdit] = React.useState(false); const [chatGptUrl, setChatGptUrl] = React.useState(''); + const [chatGptSource, setChatGptSource] = React.useState(''); + const [importJson, setImportJson] = React.useState(null); const [importOutcome, setImportOutcome] = React.useState(null); // derived state + const isUrl = importMedia === 'link'; + const isSource = importMedia === 'source'; const chatGptUrlValid = chatGptUrl.startsWith('https://chat.openai.com/share/') && chatGptUrl.length > 40; + const handleImportFromFiles = async () => { // pick file(s) let blobs: FileWithHandle[]; @@ -67,10 +79,12 @@ export function ImportConversations(props: { onClose: () => void }) { setImportOutcome(outcome); }; + const handleChatGptToggleShown = () => setChatGptEdit(!chatGptEdit); - const handleChatGptLoadFromURL = async () => { - if (!chatGptUrlValid) + const handleChatGptLoad = async () => { + setImportJson(null); + if ((isUrl && !chatGptUrlValid) || (isSource && !chatGptSource)) return; const outcome: ImportedOutcome = { conversations: [] }; @@ -78,13 +92,16 @@ export function ImportConversations(props: { onClose: () => void }) { // load the conversation let conversationId: DConversationId, data: ChatGptSharedChatSchema; try { - ({ conversationId, data } = await apiAsyncNode.trade.importChatGptShare.query({ url: chatGptUrl })); + ({ conversationId, data } = await apiAsyncNode.trade.importChatGptShare.mutate(isUrl ? { url: chatGptUrl } : { htmlPage: chatGptSource })); } catch (error) { outcome.conversations.push({ fileName: 'chatgpt', success: false, error: (error as any)?.message || error?.toString() || 'unknown error' }); setImportOutcome(outcome); return; } + // save as JSON + setImportJson(JSON.stringify(data, null, 2)); + // transform to our data structure const conversation = createDConversation(); conversation.id = conversationId; @@ -148,24 +165,31 @@ export function ImportConversations(props: { onClose: () => void }) { {chatGptEdit && - - + {importMediaControl} + + {isUrl && } + - - setChatGptUrl(event.target.value)} - /> + />} + {isSource &&