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/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/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/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/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/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..c0202e89 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')!; } @@ -637,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 78d75b80..38f06f5d 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( @@ -224,19 +255,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), }), }, ); @@ -244,7 +281,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) { @@ -394,14 +431,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/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" 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", 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" 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": { 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/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))) { 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(); }