mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) updates from grist-core
This commit is contained in:
		
						commit
						b3a71374d1
					
				
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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 <a href="https://github.com/abetlen/llama-cpp-python">llama-cpp-python</a>. | ||||
| 
 | ||||
| Sandbox related variables: | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|       } | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -20,3 +20,7 @@ export function COMMENTS(): Observable<boolean> { | ||||
| export function HAS_FORMULA_ASSISTANT() { | ||||
|   return Boolean(getGristConfig().featureFormulaAssistant); | ||||
| } | ||||
| 
 | ||||
| export function WHICH_FORMULA_ASSISTANT() { | ||||
|   return getGristConfig().assistantService; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| `);
 | ||||
|  | ||||
| @ -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) => { | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -526,6 +526,8 @@ export interface ThemeColors { | ||||
| } | ||||
| 
 | ||||
| export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>; | ||||
| export const ThemeAppearanceChecker = createCheckers(ThemePrefsTI).ThemeAppearance as CheckerT<ThemeAppearance>; | ||||
| export const ThemeNameChecker = createCheckers(ThemePrefsTI).ThemeName as CheckerT<ThemeName>; | ||||
| 
 | ||||
| export function getDefaultThemePrefs(): ThemePrefs { | ||||
|   return { | ||||
|  | ||||
| @ -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<string, string>;  // 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<GristLoadConfig>, 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; | ||||
| 
 | ||||
|  | ||||
| @ -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'); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -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(), | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -183,7 +183,7 @@ | ||||
|         "Activation": "Активация", | ||||
|         "Billing Account": "Расчетный счет", | ||||
|         "Sign In": "Войти", | ||||
|         "Sign Up": "Подписаться", | ||||
|         "Sign Up": "Зарегистрироваться", | ||||
|         "Use This Template": "Использовать этот шаблон" | ||||
|     }, | ||||
|     "ActionLog": { | ||||
|  | ||||
| @ -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<IGristUrlState>; | ||||
|     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'); | ||||
|  | ||||
| @ -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<IGristUrlState>) { | ||||
|     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'}); | ||||
|  | ||||
| @ -15,6 +15,9 @@ | ||||
|  * | ||||
|  * USAGE: | ||||
|  *  OPENAI_API_KEY=<my_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=<my_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))) { | ||||
|  | ||||
| @ -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(); | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user