From 9df62e3d81163df08622b16fa82806ab8050ae17 Mon Sep 17 00:00:00 2001 From: Philip Standt Date: Tue, 15 Aug 2023 19:29:29 +0200 Subject: [PATCH 1/6] Add appearance parameter to override theme preferences (#620) --- app/client/components/GristDoc.ts | 2 +- app/client/models/AppModel.ts | 24 +++++++++++---- app/client/ui/PagePanels.ts | 10 +++---- app/client/ui/ViewLayoutMenu.ts | 10 +++---- app/common/ThemePrefs.ts | 2 ++ app/common/gristUrls.ts | 39 ++++++++++++++++++++---- test/client/models/gristUrlState.ts | 4 +-- test/common/gristUrls.ts | 46 ++++++++++++++++++++++++++++- test/nbrowser/RawData.ts | 2 +- 9 files changed, 113 insertions(+), 26 deletions(-) diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index c615bcf6..40b8c72b 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -355,7 +355,7 @@ export class GristDoc extends DisposableWithEvents { this.autoDispose(subscribe(urlState().state, async (_use, state) => { // Only start a tour or tutorial when the full interface is showing, i.e. not when in // embedded mode. - if (state.params?.style === 'light') { + if (state.params?.style === 'singlePage') { return; } diff --git a/app/client/models/AppModel.ts b/app/client/models/AppModel.ts index abd2b44c..3de1c9c6 100644 --- a/app/client/models/AppModel.ts +++ b/app/client/models/AppModel.ts @@ -19,7 +19,7 @@ import {LocalPlugin} from 'app/common/plugin'; import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs'; import {isOwner, isOwnerOrEditor} from 'app/common/roles'; import {getTagManagerScript} from 'app/common/tagManager'; -import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs, +import {getDefaultThemePrefs, Theme, ThemeColors, ThemePrefs, ThemePrefsChecker} from 'app/common/ThemePrefs'; import {getThemeColors} from 'app/common/Themes'; import {getGristConfig} from 'app/common/urlUtils'; @@ -450,14 +450,26 @@ export class AppModelImpl extends Disposable implements AppModel { private _getCurrentThemeObs() { return Computed.create(this, this.themePrefs, prefersDarkModeObs(), (_use, themePrefs, prefersDarkMode) => { - let appearance: ThemeAppearance; - if (!themePrefs.syncWithOS) { - appearance = themePrefs.appearance; - } else { + let {appearance, syncWithOS} = themePrefs; + + const urlParams = urlState().state.get().params; + if (urlParams?.themeAppearance) { + appearance = urlParams?.themeAppearance; + } + + if (urlParams?.themeSyncWithOs !== undefined) { + syncWithOS = urlParams?.themeSyncWithOs; + } + + if (syncWithOS) { appearance = prefersDarkMode ? 'dark' : 'light'; } - const nameOrColors = themePrefs.colors[appearance]; + let nameOrColors = themePrefs.colors[appearance]; + if (urlParams?.themeName) { + nameOrColors = urlParams?.themeName; + } + let colors: ThemeColors; if (typeof nameOrColors === 'string') { colors = getThemeColors(nameOrColors); diff --git a/app/client/ui/PagePanels.ts b/app/client/ui/PagePanels.ts index a565c8e2..bfb4ae2d 100644 --- a/app/client/ui/PagePanels.ts +++ b/app/client/ui/PagePanels.ts @@ -392,7 +392,7 @@ const cssPageContainer = styled(cssVBox, ` padding-bottom: ${bottomFooterHeightPx}px; min-width: 240px; } - .interface-light & { + .interface-singlePage & { padding-bottom: 0; } } @@ -434,7 +434,7 @@ export const cssLeftPane = styled(cssVBox, ` display: none; } } - .interface-light & { + .interface-singlePage & { display: none; } &-overlap { @@ -501,7 +501,7 @@ const cssRightPane = styled(cssVBox, ` display: none; } } - .interface-light & { + .interface-singlePage & { display: none; } `); @@ -519,7 +519,7 @@ const cssHeader = styled('div', ` } } - .interface-light & { + .interface-singlePage & { display: none; } `); @@ -556,7 +556,7 @@ const cssBottomFooter = styled ('div', ` display: none; } } - .interface-light & { + .interface-singlePage & { display: none; } `); diff --git a/app/client/ui/ViewLayoutMenu.ts b/app/client/ui/ViewLayoutMenu.ts index 67a60ec4..4e60490b 100644 --- a/app/client/ui/ViewLayoutMenu.ts +++ b/app/client/ui/ViewLayoutMenu.ts @@ -36,7 +36,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool ]; const viewRec = viewSection.view(); - const isLight = urlState().state.get().params?.style === 'light'; + const isSinglePage = urlState().state.get().params?.style === 'singlePage'; const sectionId = viewSection.table.peek().rawViewSectionRef.peek(); const anchorUrlState = viewInstance.getAnchorLinkForSection(sectionId); @@ -57,7 +57,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool const showRawData = (use: UseCB) => { return !use(viewSection.isRaw)// Don't show raw data if we're already in raw data. - && !isLight // Don't show raw data in light mode. + && !isSinglePage // Don't show raw data in single page mode. ; }; @@ -84,7 +84,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool menuItemCmd(allCommands.editLayout, t("Edit Card Layout"), dom.cls('disabled', isReadonly))), - dom.maybe(!isLight, () => [ + dom.maybe(!isSinglePage, () => [ menuDivider(), menuItemCmd(allCommands.viewTabOpen, t("Widget options"), testId('widget-options')), menuItemCmd(allCommands.sortFilterTabOpen, t("Advanced Sort & Filter")), @@ -111,12 +111,12 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool */ export function makeCollapsedLayoutMenu(viewSection: ViewSectionRec, gristDoc: GristDoc) { const isReadonly = gristDoc.isReadonly.get(); - const isLight = urlState().state.get().params?.style === 'light'; + const isSinglePage = urlState().state.get().params?.style === 'singlePage'; const sectionId = viewSection.table.peek().rawViewSectionRef.peek(); const anchorUrlState = { hash: { sectionId, popup: true } }; const rawUrl = urlState().makeUrl(anchorUrlState); return [ - dom.maybe((use) => !use(viewSection.isRaw) && !isLight && !use(gristDoc.maximizedSectionId), + dom.maybe((use) => !use(viewSection.isRaw) && !isSinglePage && !use(gristDoc.maximizedSectionId), () => menuItemLink( { href: rawUrl}, t("Show raw data"), testId('show-raw-data'), dom.on('click', (ev) => { diff --git a/app/common/ThemePrefs.ts b/app/common/ThemePrefs.ts index 308fe79b..aee78492 100644 --- a/app/common/ThemePrefs.ts +++ b/app/common/ThemePrefs.ts @@ -526,6 +526,8 @@ export interface ThemeColors { } export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT; +export const ThemeAppearanceChecker = createCheckers(ThemePrefsTI).ThemeAppearance as CheckerT; +export const ThemeNameChecker = createCheckers(ThemePrefsTI).ThemeName as CheckerT; export function getDefaultThemePrefs(): ThemePrefs { return { diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 2af7dae7..e7541322 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -10,6 +10,7 @@ import {getGristConfig} from 'app/common/urlUtils'; import {Document} from 'app/common/UserAPI'; import clone = require('lodash/clone'); import pickBy = require('lodash/pickBy'); +import {ThemeAppearance, ThemeAppearanceChecker, ThemeName, ThemeNameChecker} from './ThemePrefs'; export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook'); type SpecialDocPage = typeof SpecialDocPage.type; @@ -44,8 +45,8 @@ export type LoginPage = typeof LoginPage.type; export const SupportGristPage = StringUnion('support-grist'); export type SupportGristPage = typeof SupportGristPage.type; -// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience. -export const InterfaceStyle = StringUnion('light', 'full'); +// Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience. +export const InterfaceStyle = StringUnion('singlePage', 'full'); export type InterfaceStyle = typeof InterfaceStyle.type; // Default subdomain for home api service if not otherwise specified. @@ -126,6 +127,9 @@ export interface IGristUrlState { compare?: string; linkParameters?: Record; // Parameters to pass as 'user.Link' in granular ACLs. // Encoded in URL as query params with extra '_' suffix. + themeSyncWithOs?: boolean; + themeAppearance?: ThemeAppearance; + themeName?: ThemeName; }; hash?: HashLink; // if present, this specifies an individual row within a section of a page. } @@ -392,15 +396,40 @@ export function decodeUrl(gristConfig: Partial, location: Locat } if (sp.has('style')) { - state.params!.style = InterfaceStyle.parse(sp.get('style')); + let style = sp.get('style'); + if (style === 'light') { + style = 'singlePage'; + } + + state.params!.style = InterfaceStyle.parse(style); } if (sp.has('embed')) { const embed = state.params!.embed = isAffirmative(sp.get('embed')); // Turn view mode on if no mode has been specified, and not a fork. if (embed && !state.mode && !state.fork) { state.mode = 'view'; } - // Turn on light style if no style has been specified. - if (embed && !state.params!.style) { state.params!.style = 'light'; } + // Turn on single page style if no style has been specified. + if (embed && !state.params!.style) { state.params!.style = 'singlePage'; } + } + + // Theme overrides + if (sp.has('themeSyncWithOs')) { + state.params!.themeSyncWithOs = isAffirmative(sp.get('themeSyncWithOs')); } + + if (sp.has('themeAppearance')) { + const appearance = sp.get('themeAppearance'); + if (ThemeAppearanceChecker.strictTest(appearance)) { + state.params!.themeAppearance = appearance; + } + } + + if (sp.has('themeName')) { + const themeName = sp.get('themeName'); + if (ThemeNameChecker.strictTest(themeName)) { + state.params!.themeName = themeName; + } + } + if (sp.has('compare')) { state.params!.compare = sp.get('compare')!; } diff --git a/test/client/models/gristUrlState.ts b/test/client/models/gristUrlState.ts index 67c31dda..04d7dae3 100644 --- a/test/client/models/gristUrlState.ts +++ b/test/client/models/gristUrlState.ts @@ -287,8 +287,8 @@ describe('gristUrlState', function() { it('should support an update function to pushUrl and makeUrl', async function() { mockWindow.location = new URL('https://bar.example.com/doc/DOC/p/5') as unknown as Location; const state = UrlState.create(null, mockWindow, prod) as UrlState; - await state.pushUrl({params: {style: 'light', linkParameters: {foo: 'A', bar: 'B'}}}); - assert.equal(mockWindow.location.href, 'https://bar.example.com/doc/DOC/p/5?style=light&foo_=A&bar_=B'); + await state.pushUrl({params: {style: 'singlePage', linkParameters: {foo: 'A', bar: 'B'}}}); + assert.equal(mockWindow.location.href, 'https://bar.example.com/doc/DOC/p/5?style=singlePage&foo_=A&bar_=B'); state.loadState(); // changing linkParameters requires a page reload assert.equal(state.makeUrl((prevState) => merge({}, prevState, {params: {style: 'full'}})), 'https://bar.example.com/doc/DOC/p/5?style=full&foo_=A&bar_=B'); diff --git a/test/common/gristUrls.ts b/test/common/gristUrls.ts index fad0dbcb..54e09b00 100644 --- a/test/common/gristUrls.ts +++ b/test/common/gristUrls.ts @@ -1,8 +1,52 @@ -import {parseFirstUrlPart} from 'app/common/gristUrls'; +import {decodeUrl, IGristUrlState, parseFirstUrlPart} from 'app/common/gristUrls'; import {assert} from 'chai'; describe('gristUrls', function() { + function assertUrlDecode(url: string, expected: Partial) { + const actual = decodeUrl({}, new URL(url)); + + for (const property in expected) { + const expectedValue = expected[property as keyof IGristUrlState]; + const actualValue = actual[property as keyof IGristUrlState]; + + assert.deepEqual(actualValue, expectedValue); + } + } + + describe('encodeUrl', function() { + it('should detect theme appearance override', function() { + assertUrlDecode( + 'http://localhost/?themeAppearance=light', + {params: {themeAppearance: 'light'}}, + ); + + assertUrlDecode( + 'http://localhost/?themeAppearance=dark', + {params: {themeAppearance: 'dark'}}, + ); + }); + + it('should detect theme sync with os override', function() { + assertUrlDecode( + 'http://localhost/?themeSyncWithOs=true', + {params: {themeSyncWithOs: true}}, + ); + }); + + it('should detect theme name override', function() { + assertUrlDecode( + 'http://localhost/?themeName=GristLight', + {params: {themeName: 'GristLight'}}, + ); + + assertUrlDecode( + 'http://localhost/?themeName=GristDark', + {params: {themeName: 'GristDark'}}, + ); + }); + }); + describe('parseFirstUrlPart', function() { it('should strip out matching tag', function() { assert.deepEqual(parseFirstUrlPart('o', '/o/foo/bar?x#y'), {value: 'foo', path: '/bar?x#y'}); diff --git a/test/nbrowser/RawData.ts b/test/nbrowser/RawData.ts index c1749f6c..091c90f2 100644 --- a/test/nbrowser/RawData.ts +++ b/test/nbrowser/RawData.ts @@ -675,7 +675,7 @@ async function openMenu(tableId: string) { } async function waitForRawData() { - await driver.findWait('.test-raw-data-list', 1000); + await driver.findWait('.test-raw-data-list', 2000); await gu.waitForServer(); } From 1774d0314fd33d8a97151f428249398878a17c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=20=D0=92?= Date: Tue, 15 Aug 2023 19:41:57 +0000 Subject: [PATCH 2/6] Translated using Weblate (Russian) Currently translated at 99.6% (948 of 951 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/ru/ --- static/locales/ru.client.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index f9ac25b4..7e20a498 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -183,7 +183,7 @@ "Activation": "Активация", "Billing Account": "Расчетный счет", "Sign In": "Войти", - "Sign Up": "Подписаться", + "Sign Up": "Зарегистрироваться", "Use This Template": "Использовать этот шаблон" }, "ActionLog": { From 5705c37d02e39e2a4c9e34a8b15647e3605a4adf Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Wed, 16 Aug 2023 20:55:16 +0000 Subject: [PATCH 3/6] Translated using Weblate (Spanish) Currently translated at 100.0% (951 of 951 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 4d11ebde..6e60d9b0 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -1132,7 +1132,7 @@ "I can only help with formulas. I cannot build tables, columns, and views, or write access rules.": "Sólo puedo ayudar con fórmulas. No puedo construir tablas, columnas y vistas, ni escribir reglas de acceso.", "Sign Up for Free": "Regístrate gratis", "There are some things you should know when working with me:": "Hay algunas cosas que debes saber cuando trabajes conmigo:", - "Formula AI Assistant is only available for logged in users.": "Formula AI Assistant sólo está disponible para usuarios registrados." + "Formula AI Assistant is only available for logged in users.": "Asistente de Fórmula de IA sólo está disponible para usuarios registrados." }, "GridView": { "Click to insert": "Haga clic para insertar" From 0be858c19d79f8b41a16c8700bd69911d9ff5cbb Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Fri, 18 Aug 2023 16:14:42 -0400 Subject: [PATCH 4/6] allow AI Assistance to run against any chat-completion-style endpoint (#630) This adds an ASSISTANT_CHAT_COMPLETION_ENDPOINT which can be used to enable AI Assistance instead of an OpenAI API key. The assistant then works against compatible endpoints, in the mechanical sense. Quality of course will depend on the model. I found some tweaks to the prompt that work well both for Llama-2 and for OpenAI's models, but I'm not including them here because they would conflict with some prompt changes that are already in the works. Co-authored-by: Alex Hall --- README.md | 16 +++++- app/client/models/features.ts | 4 ++ app/client/widgets/FormulaAssistant.ts | 6 +- app/common/gristUrls.ts | 4 ++ app/server/lib/Assistance.ts | 65 ++++++++++++++++------ app/server/lib/sendAppPage.ts | 3 +- test/formula-dataset/runCompletion_impl.ts | 6 +- 7 files changed, 81 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7bbe7ff0..25441eee 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Here are some specific feature highlights of Grist: - On OSX, you can use native sandboxing. - On any OS, including Windows, you can use a wasm-based sandbox. * Translated to many languages. - * Support for an AI Formula Assistant (using OpenAI gpt-3.5-turbo). + * Support for an AI Formula Assistant (using OpenAI gpt-3.5-turbo or comparable models). * `F1` key brings up some quick help. This used to go without saying. In general Grist has good keyboard support. * We post progress on [𝕏 or Twitter or whatever](https://twitter.com/getgrist). @@ -302,7 +302,19 @@ PORT | port number to listen on for Grist server REDIS_URL | optional redis server for browser sessions and db query caching GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000} GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made -OPENAI_API_KEY | optional. Used for the AI formula assistant. Sign up for an account on OpenAI and then generate a secret key [here](https://platform.openai.com/account/api-keys). + +AI Formula Assistant related variables (all optional): + +Variable | Purpose +-------- | ------- +ASSISTANT_API_KEY | optional. An API key to pass when making requests to an external AI conversational endpoint. +ASSISTANT_CHAT_COMPLETION_ENDPOINT | optional. A chat-completion style endpoint to call. Not needed if OpenAI is being used. +ASSISTANT_MODEL | optional. If set, this string is passed along in calls to the AI conversational endpoint. +ASSISTANT_LONGER_CONTEXT_MODEL | optional. If set, requests that fail because of a context length limitation will be retried with this model set. +OPENAI_API_KEY | optional. Synonym for ASSISTANT_API_KEY that assumes an OpenAI endpoint is being used. Sign up for an account on OpenAI and then generate a secret key [here](https://platform.openai.com/account/api-keys). + +At the time of writing, the AI Assistant is known to function against OpenAI chat completion endpoints for gpt-3.5-turbo and gpt-4. +It can also function against the chat completion endpoint provided by llama-cpp-python. Sandbox related variables: diff --git a/app/client/models/features.ts b/app/client/models/features.ts index aa883e52..d19a8d28 100644 --- a/app/client/models/features.ts +++ b/app/client/models/features.ts @@ -20,3 +20,7 @@ export function COMMENTS(): Observable { export function HAS_FORMULA_ASSISTANT() { return Boolean(getGristConfig().featureFormulaAssistant); } + +export function WHICH_FORMULA_ASSISTANT() { + return getGristConfig().assistantService; +} diff --git a/app/client/widgets/FormulaAssistant.ts b/app/client/widgets/FormulaAssistant.ts index aaeae3be..8fc9c827 100644 --- a/app/client/widgets/FormulaAssistant.ts +++ b/app/client/widgets/FormulaAssistant.ts @@ -6,7 +6,7 @@ import {movable} from 'app/client/lib/popupUtils'; import {logTelemetryEvent} from 'app/client/lib/telemetry'; import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel'; import {ChatMessage} from 'app/client/models/entities/ColumnRec'; -import {HAS_FORMULA_ASSISTANT} from 'app/client/models/features'; +import {HAS_FORMULA_ASSISTANT, WHICH_FORMULA_ASSISTANT} from 'app/client/models/features'; import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState'; import {buildHighlightedCode} from 'app/client/ui/CodeHighlight'; import {autoGrow} from 'app/client/ui/forms'; @@ -879,7 +879,7 @@ class ChatHistory extends Disposable { '"Please calculate the total invoice amount."' ), ), - cssAiMessageBullet( + (WHICH_FORMULA_ASSISTANT() === 'OpenAI') ? cssAiMessageBullet( cssTickIcon('Tick'), dom('div', t( @@ -891,7 +891,7 @@ class ChatHistory extends Disposable { } ), ), - ), + ) : null, ), cssAiMessageParagraph( t( diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index e7541322..c0202e89 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -666,6 +666,10 @@ export interface GristLoadConfig { // TODO: remove once released. featureFormulaAssistant?: boolean; + // Used to determine which disclosure links should be provided to user of + // formula assistance. + assistantService?: 'OpenAI' | undefined; + // Email address of the support user. supportEmail?: string; diff --git a/app/server/lib/Assistance.ts b/app/server/lib/Assistance.ts index 0ece1d65..943eb1ea 100644 --- a/app/server/lib/Assistance.ts +++ b/app/server/lib/Assistance.ts @@ -117,23 +117,54 @@ class RetryableError extends Error { } /** - * A flavor of assistant for use with the OpenAI API. + * A flavor of assistant for use with the OpenAI chat completion endpoint + * and tools with a compatible endpoint (e.g. llama-cpp-python). * Tested primarily with gpt-3.5-turbo. + * + * Uses the ASSISTANT_CHAT_COMPLETION_ENDPOINT endpoint if set, else + * an OpenAI endpoint. Passes ASSISTANT_API_KEY or OPENAI_API_KEY in + * a header if set. An api key is required for the default OpenAI + * endpoint. + * + * If a model string is set in ASSISTANT_MODEL, this will be passed + * along. For the default OpenAI endpoint, a gpt-3.5-turbo variant + * will be set by default. + * + * If a request fails because of context length limitation, and the + * default OpenAI endpoint is in use, the request will be retried + * with ASSISTANT_LONGER_CONTEXT_MODEL (another gpt-3.5 + * variant by default). Set this variable to "" if this behavior is + * not desired for the default OpenAI endpoint. If a custom endpoint was + * provided, this behavior will only happen if + * ASSISTANT_LONGER_CONTEXT_MODEL is explicitly set. + * + * An optional ASSISTANT_MAX_TOKENS can be specified. */ export class OpenAIAssistant implements Assistant { public static DEFAULT_MODEL = "gpt-3.5-turbo-0613"; - public static LONGER_CONTEXT_MODEL = "gpt-3.5-turbo-16k-0613"; + public static DEFAULT_LONGER_CONTEXT_MODEL = "gpt-3.5-turbo-16k-0613"; - private _apiKey: string; + private _apiKey?: string; + private _model?: string; + private _longerContextModel?: string; private _endpoint: string; + private _maxTokens = process.env.ASSISTANT_MAX_TOKENS ? + parseInt(process.env.ASSISTANT_MAX_TOKENS, 10) : undefined; public constructor() { - const apiKey = process.env.OPENAI_API_KEY; - if (!apiKey) { - throw new Error('OPENAI_API_KEY not set'); + const apiKey = process.env.ASSISTANT_API_KEY || process.env.OPENAI_API_KEY; + const endpoint = process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT; + if (!apiKey && !endpoint) { + throw new Error('Please set either OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT'); } this._apiKey = apiKey; - this._endpoint = `https://api.openai.com/v1/chat/completions`; + this._model = process.env.ASSISTANT_MODEL; + this._longerContextModel = process.env.ASSISTANT_LONGER_CONTEXT_MODEL; + if (!endpoint) { + this._model = this._model ?? OpenAIAssistant.DEFAULT_MODEL; + this._longerContextModel = this._longerContextModel ?? OpenAIAssistant.DEFAULT_LONGER_CONTEXT_MODEL; + } + this._endpoint = endpoint || `https://api.openai.com/v1/chat/completions`; } public async apply( @@ -222,19 +253,25 @@ export class OpenAIAssistant implements Assistant { } private async _fetchCompletion(messages: AssistanceMessage[], userIdHash: string, longerContext: boolean) { + const model = longerContext ? this._longerContextModel : this._model; const apiResponse = await DEPS.fetch( this._endpoint, { method: "POST", headers: { - "Authorization": `Bearer ${this._apiKey}`, + ...(this._apiKey ? { + "Authorization": `Bearer ${this._apiKey}`, + } : undefined), "Content-Type": "application/json", }, body: JSON.stringify({ messages, temperature: 0, - model: longerContext ? OpenAIAssistant.LONGER_CONTEXT_MODEL : OpenAIAssistant.DEFAULT_MODEL, + ...(model ? { model } : undefined), user: userIdHash, + ...(this._maxTokens ? { + max_tokens: this._maxTokens, + } : undefined), }), }, ); @@ -242,7 +279,7 @@ export class OpenAIAssistant implements Assistant { const result = JSON.parse(resultText); const errorCode = result.error?.code; if (errorCode === "context_length_exceeded" || result.choices?.[0].finish_reason === "length") { - if (!longerContext) { + if (!longerContext && this._longerContextModel) { log.info("Switching to longer context model..."); throw new SwitchToLongerContext(); } else if (messages.length <= 2) { @@ -392,14 +429,10 @@ export function getAssistant() { if (process.env.OPENAI_API_KEY === 'test') { return new EchoAssistant(); } - if (process.env.OPENAI_API_KEY) { + if (process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT) { return new OpenAIAssistant(); } - // Maintaining this is too much of a burden for now. - // if (process.env.HUGGINGFACE_API_KEY) { - // return new HuggingFaceAssistant(); - // } - throw new Error('Please set OPENAI_API_KEY'); + throw new Error('Please set OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT'); } /** diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index 03cbf7b0..ca5b31ff 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -74,7 +74,8 @@ export function makeGristConfig(options: MakeGristConfigOptons): GristLoadConfig supportedLngs: readLoadedLngs(req?.i18n), namespaces: readLoadedNamespaces(req?.i18n), featureComments: isAffirmative(process.env.COMMENTS), - featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY), + featureFormulaAssistant: Boolean(process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT), + assistantService: process.env.OPENAI_API_KEY ? 'OpenAI' : undefined, supportEmail: SUPPORT_EMAIL, userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale, telemetry: server?.getTelemetry().getTelemetryConfig(), diff --git a/test/formula-dataset/runCompletion_impl.ts b/test/formula-dataset/runCompletion_impl.ts index 2eb92594..5582faf2 100644 --- a/test/formula-dataset/runCompletion_impl.ts +++ b/test/formula-dataset/runCompletion_impl.ts @@ -15,6 +15,9 @@ * * USAGE: * OPENAI_API_KEY= node core/test/formula-dataset/runCompletion.js + * or + * ASSISTANT_CHAT_COMPLETION_ENDPOINT=http.... node core/test/formula-dataset/runCompletion.js + * (see Assistance.ts for more options). * * # WITH VERBOSE: * VERBOSE=1 OPENAI_API_KEY= node core/test/formula-dataset/runCompletion.js @@ -68,7 +71,8 @@ const SIMULATE_CONVERSATION = true; const FOLLOWUP_EVALUATE = false; export async function runCompletion() { - ActiveDocDeps.ACTIVEDOC_TIMEOUT = 600; + // This could take a long time for LLMs running on underpowered hardware >:) + ActiveDocDeps.ACTIVEDOC_TIMEOUT = 500000; // if template directory not exists, make it if (!fs.existsSync(path.join(PATH_TO_DOC))) { From 9b62fd4b68ac227cf761d0ee77d6ff2d112507a6 Mon Sep 17 00:00:00 2001 From: Riccardo Polignieri Date: Fri, 18 Aug 2023 20:39:15 +0000 Subject: [PATCH 5/6] Translated using Weblate (Italian) Currently translated at 100.0% (951 of 951 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/it/ --- static/locales/it.client.json | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/static/locales/it.client.json b/static/locales/it.client.json index 9a8d9bc1..740310d1 100644 --- a/static/locales/it.client.json +++ b/static/locales/it.client.json @@ -67,7 +67,22 @@ "Importer": { "Update existing records": "Aggiorna i record esistenti", "Merge rows that match these fields:": "Unisci le righe che corrispondono a questi campi:", - "Select fields to match on": "Seleziona i campi da far corrispondere" + "Select fields to match on": "Seleziona i campi da far corrispondere", + "Column Mapping": "Corrispondenze nelle colonne", + "Column mapping": "Corrispondenze nelle colonne", + "Destination table": "Tabella di destinazione", + "Grist column": "Colonna di Grist", + "Import from file": "Importa da file", + "New Table": "Nuova tabella", + "Revert": "Ripristina", + "Skip": "Salta", + "{{count}} unmatched field in import_one": "{{count}} campi senza equivalente nell'importazione", + "{{count}} unmatched field in import_other": "{{count}} campi senza equivalente nell'importazione", + "{{count}} unmatched field_one": "{{count}} campi senza equivalente", + "{{count}} unmatched field_other": "{{count}} campi senza equivalente", + "Skip Import": "Salta l'importazione", + "Skip Table on Import": "Salta la tabella nell'importazione", + "Source column": "Colonna di origine" }, "NotifyUI": { "Cannot find personal site, sorry!": "Spiacente, impossibile trovare il sito personale!", @@ -236,7 +251,10 @@ "CELL STYLE": "STILE CELLA", "Mixed style": "Stile misto", "Open row styles": "Apri stili riga", - "Default cell style": "Stile cella di default" + "Default cell style": "Stile cella di default", + "Default header style": "Stile di default per l'intestazione", + "Header Style": "Stile per l'intestazione", + "HEADER STYLE": "STILE INTESTAZIONE" }, "ChoiceTextBox": { "CHOICES": "SCELTE" @@ -1059,7 +1077,8 @@ "Sign Up for Free": "Iscriviti gratis", "There are some things you should know when working with me:": "Ecco alcune cose da sapere quando lavori con me:", "What do you need help with?": "In che cosa posso aiutarti?", - "Sign up for a free Grist account to start using the Formula AI Assistant.": "Iscriviti a un account gratuito di Grist per usare l'Assistente IA per le formule." + "Sign up for a free Grist account to start using the Formula AI Assistant.": "Iscriviti a un account gratuito di Grist per usare l'Assistente IA per le formule.", + "Formula AI Assistant is only available for logged in users.": "L'assistente IA per le formule è disponibile solo dopo aver effettuato l'accesso." }, "GridView": { "Click to insert": "Clicca per inserire" From 69f43f061baa94786d2723d5798d38f016d392af Mon Sep 17 00:00:00 2001 From: Camille L Date: Mon, 21 Aug 2023 09:36:48 +0000 Subject: [PATCH 6/6] Translated using Weblate (French) Currently translated at 94.2% (896 of 951 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/fr/ --- static/locales/fr.client.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/locales/fr.client.json b/static/locales/fr.client.json index ed993338..e3ac7e13 100644 --- a/static/locales/fr.client.json +++ b/static/locales/fr.client.json @@ -830,7 +830,9 @@ "Cell Style": "Style de cellule", "CELL STYLE": "STYLE de CELLULE", "Default cell style": "Style par défaut", - "Mixed style": "Style composite" + "Mixed style": "Style composite", + "Header Style": "Style de l'entête", + "Default header style": "Style par défaut" }, "DiscussionEditor": { "Comment": "Commentaire",