mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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 OSX, you can use native sandboxing.
|
||||||
- On any OS, including Windows, you can use a wasm-based sandbox.
|
- On any OS, including Windows, you can use a wasm-based sandbox.
|
||||||
* Translated to many languages.
|
* 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.
|
* `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).
|
* 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
|
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_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
|
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:
|
Sandbox related variables:
|
||||||
|
|
||||||
|
@ -355,7 +355,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
this.autoDispose(subscribe(urlState().state, async (_use, state) => {
|
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
|
// Only start a tour or tutorial when the full interface is showing, i.e. not when in
|
||||||
// embedded mode.
|
// embedded mode.
|
||||||
if (state.params?.style === 'light') {
|
if (state.params?.style === 'singlePage') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import {LocalPlugin} from 'app/common/plugin';
|
|||||||
import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
|
import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
|
||||||
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
|
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
|
||||||
import {getTagManagerScript} from 'app/common/tagManager';
|
import {getTagManagerScript} from 'app/common/tagManager';
|
||||||
import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
|
import {getDefaultThemePrefs, Theme, ThemeColors, ThemePrefs,
|
||||||
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
||||||
import {getThemeColors} from 'app/common/Themes';
|
import {getThemeColors} from 'app/common/Themes';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
@ -450,14 +450,26 @@ export class AppModelImpl extends Disposable implements AppModel {
|
|||||||
private _getCurrentThemeObs() {
|
private _getCurrentThemeObs() {
|
||||||
return Computed.create(this, this.themePrefs, prefersDarkModeObs(),
|
return Computed.create(this, this.themePrefs, prefersDarkModeObs(),
|
||||||
(_use, themePrefs, prefersDarkMode) => {
|
(_use, themePrefs, prefersDarkMode) => {
|
||||||
let appearance: ThemeAppearance;
|
let {appearance, syncWithOS} = themePrefs;
|
||||||
if (!themePrefs.syncWithOS) {
|
|
||||||
appearance = themePrefs.appearance;
|
const urlParams = urlState().state.get().params;
|
||||||
} else {
|
if (urlParams?.themeAppearance) {
|
||||||
|
appearance = urlParams?.themeAppearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParams?.themeSyncWithOs !== undefined) {
|
||||||
|
syncWithOS = urlParams?.themeSyncWithOs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncWithOS) {
|
||||||
appearance = prefersDarkMode ? 'dark' : 'light';
|
appearance = prefersDarkMode ? 'dark' : 'light';
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameOrColors = themePrefs.colors[appearance];
|
let nameOrColors = themePrefs.colors[appearance];
|
||||||
|
if (urlParams?.themeName) {
|
||||||
|
nameOrColors = urlParams?.themeName;
|
||||||
|
}
|
||||||
|
|
||||||
let colors: ThemeColors;
|
let colors: ThemeColors;
|
||||||
if (typeof nameOrColors === 'string') {
|
if (typeof nameOrColors === 'string') {
|
||||||
colors = getThemeColors(nameOrColors);
|
colors = getThemeColors(nameOrColors);
|
||||||
|
@ -20,3 +20,7 @@ export function COMMENTS(): Observable<boolean> {
|
|||||||
export function HAS_FORMULA_ASSISTANT() {
|
export function HAS_FORMULA_ASSISTANT() {
|
||||||
return Boolean(getGristConfig().featureFormulaAssistant);
|
return Boolean(getGristConfig().featureFormulaAssistant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function WHICH_FORMULA_ASSISTANT() {
|
||||||
|
return getGristConfig().assistantService;
|
||||||
|
}
|
||||||
|
@ -392,7 +392,7 @@ const cssPageContainer = styled(cssVBox, `
|
|||||||
padding-bottom: ${bottomFooterHeightPx}px;
|
padding-bottom: ${bottomFooterHeightPx}px;
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
.interface-light & {
|
.interface-singlePage & {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -434,7 +434,7 @@ export const cssLeftPane = styled(cssVBox, `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.interface-light & {
|
.interface-singlePage & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&-overlap {
|
&-overlap {
|
||||||
@ -501,7 +501,7 @@ const cssRightPane = styled(cssVBox, `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.interface-light & {
|
.interface-singlePage & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -519,7 +519,7 @@ const cssHeader = styled('div', `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.interface-light & {
|
.interface-singlePage & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
@ -556,7 +556,7 @@ const cssBottomFooter = styled ('div', `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.interface-light & {
|
.interface-singlePage & {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -36,7 +36,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
|
|||||||
];
|
];
|
||||||
|
|
||||||
const viewRec = viewSection.view();
|
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 sectionId = viewSection.table.peek().rawViewSectionRef.peek();
|
||||||
const anchorUrlState = viewInstance.getAnchorLinkForSection(sectionId);
|
const anchorUrlState = viewInstance.getAnchorLinkForSection(sectionId);
|
||||||
@ -57,7 +57,7 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
|
|||||||
|
|
||||||
const showRawData = (use: UseCB) => {
|
const showRawData = (use: UseCB) => {
|
||||||
return !use(viewSection.isRaw)// Don't show raw data if we're already in raw data.
|
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"),
|
menuItemCmd(allCommands.editLayout, t("Edit Card Layout"),
|
||||||
dom.cls('disabled', isReadonly))),
|
dom.cls('disabled', isReadonly))),
|
||||||
|
|
||||||
dom.maybe(!isLight, () => [
|
dom.maybe(!isSinglePage, () => [
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
menuItemCmd(allCommands.viewTabOpen, t("Widget options"), testId('widget-options')),
|
menuItemCmd(allCommands.viewTabOpen, t("Widget options"), testId('widget-options')),
|
||||||
menuItemCmd(allCommands.sortFilterTabOpen, t("Advanced Sort & Filter")),
|
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) {
|
export function makeCollapsedLayoutMenu(viewSection: ViewSectionRec, gristDoc: GristDoc) {
|
||||||
const isReadonly = gristDoc.isReadonly.get();
|
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 sectionId = viewSection.table.peek().rawViewSectionRef.peek();
|
||||||
const anchorUrlState = { hash: { sectionId, popup: true } };
|
const anchorUrlState = { hash: { sectionId, popup: true } };
|
||||||
const rawUrl = urlState().makeUrl(anchorUrlState);
|
const rawUrl = urlState().makeUrl(anchorUrlState);
|
||||||
return [
|
return [
|
||||||
dom.maybe((use) => !use(viewSection.isRaw) && !isLight && !use(gristDoc.maximizedSectionId),
|
dom.maybe((use) => !use(viewSection.isRaw) && !isSinglePage && !use(gristDoc.maximizedSectionId),
|
||||||
() => menuItemLink(
|
() => menuItemLink(
|
||||||
{ href: rawUrl}, t("Show raw data"), testId('show-raw-data'),
|
{ href: rawUrl}, t("Show raw data"), testId('show-raw-data'),
|
||||||
dom.on('click', (ev) => {
|
dom.on('click', (ev) => {
|
||||||
|
@ -6,7 +6,7 @@ import {movable} from 'app/client/lib/popupUtils';
|
|||||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||||
import {ChatMessage} from 'app/client/models/entities/ColumnRec';
|
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 {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
|
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
|
||||||
import {autoGrow} from 'app/client/ui/forms';
|
import {autoGrow} from 'app/client/ui/forms';
|
||||||
@ -879,7 +879,7 @@ class ChatHistory extends Disposable {
|
|||||||
'"Please calculate the total invoice amount."'
|
'"Please calculate the total invoice amount."'
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cssAiMessageBullet(
|
(WHICH_FORMULA_ASSISTANT() === 'OpenAI') ? cssAiMessageBullet(
|
||||||
cssTickIcon('Tick'),
|
cssTickIcon('Tick'),
|
||||||
dom('div',
|
dom('div',
|
||||||
t(
|
t(
|
||||||
@ -891,7 +891,7 @@ class ChatHistory extends Disposable {
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
) : null,
|
||||||
),
|
),
|
||||||
cssAiMessageParagraph(
|
cssAiMessageParagraph(
|
||||||
t(
|
t(
|
||||||
|
@ -526,6 +526,8 @@ export interface ThemeColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ThemePrefsChecker = createCheckers(ThemePrefsTI).ThemePrefs as CheckerT<ThemePrefs>;
|
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 {
|
export function getDefaultThemePrefs(): ThemePrefs {
|
||||||
return {
|
return {
|
||||||
|
@ -10,6 +10,7 @@ import {getGristConfig} from 'app/common/urlUtils';
|
|||||||
import {Document} from 'app/common/UserAPI';
|
import {Document} from 'app/common/UserAPI';
|
||||||
import clone = require('lodash/clone');
|
import clone = require('lodash/clone');
|
||||||
import pickBy = require('lodash/pickBy');
|
import pickBy = require('lodash/pickBy');
|
||||||
|
import {ThemeAppearance, ThemeAppearanceChecker, ThemeName, ThemeNameChecker} from './ThemePrefs';
|
||||||
|
|
||||||
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook');
|
export const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour', 'settings', 'webhook');
|
||||||
type SpecialDocPage = typeof SpecialDocPage.type;
|
type SpecialDocPage = typeof SpecialDocPage.type;
|
||||||
@ -44,8 +45,8 @@ export type LoginPage = typeof LoginPage.type;
|
|||||||
export const SupportGristPage = StringUnion('support-grist');
|
export const SupportGristPage = StringUnion('support-grist');
|
||||||
export type SupportGristPage = typeof SupportGristPage.type;
|
export type SupportGristPage = typeof SupportGristPage.type;
|
||||||
|
|
||||||
// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience.
|
// Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience.
|
||||||
export const InterfaceStyle = StringUnion('light', 'full');
|
export const InterfaceStyle = StringUnion('singlePage', 'full');
|
||||||
export type InterfaceStyle = typeof InterfaceStyle.type;
|
export type InterfaceStyle = typeof InterfaceStyle.type;
|
||||||
|
|
||||||
// Default subdomain for home api service if not otherwise specified.
|
// Default subdomain for home api service if not otherwise specified.
|
||||||
@ -126,6 +127,9 @@ export interface IGristUrlState {
|
|||||||
compare?: string;
|
compare?: string;
|
||||||
linkParameters?: Record<string, string>; // Parameters to pass as 'user.Link' in granular ACLs.
|
linkParameters?: Record<string, string>; // Parameters to pass as 'user.Link' in granular ACLs.
|
||||||
// Encoded in URL as query params with extra '_' suffix.
|
// 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.
|
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')) {
|
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')) {
|
if (sp.has('embed')) {
|
||||||
const embed = state.params!.embed = isAffirmative(sp.get('embed'));
|
const embed = state.params!.embed = isAffirmative(sp.get('embed'));
|
||||||
// Turn view mode on if no mode has been specified, and not a fork.
|
// Turn view mode on if no mode has been specified, and not a fork.
|
||||||
if (embed && !state.mode && !state.fork) { state.mode = 'view'; }
|
if (embed && !state.mode && !state.fork) { state.mode = 'view'; }
|
||||||
// Turn on light style if no style has been specified.
|
// Turn on single page style if no style has been specified.
|
||||||
if (embed && !state.params!.style) { state.params!.style = 'light'; }
|
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')) {
|
if (sp.has('compare')) {
|
||||||
state.params!.compare = sp.get('compare')!;
|
state.params!.compare = sp.get('compare')!;
|
||||||
}
|
}
|
||||||
@ -637,6 +666,10 @@ export interface GristLoadConfig {
|
|||||||
// TODO: remove once released.
|
// TODO: remove once released.
|
||||||
featureFormulaAssistant?: boolean;
|
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.
|
// Email address of the support user.
|
||||||
supportEmail?: string;
|
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.
|
* 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 {
|
export class OpenAIAssistant implements Assistant {
|
||||||
public static DEFAULT_MODEL = "gpt-3.5-turbo-0613";
|
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 _endpoint: string;
|
||||||
|
private _maxTokens = process.env.ASSISTANT_MAX_TOKENS ?
|
||||||
|
parseInt(process.env.ASSISTANT_MAX_TOKENS, 10) : undefined;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
const apiKey = process.env.OPENAI_API_KEY;
|
const apiKey = process.env.ASSISTANT_API_KEY || process.env.OPENAI_API_KEY;
|
||||||
if (!apiKey) {
|
const endpoint = process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT;
|
||||||
throw new Error('OPENAI_API_KEY not set');
|
if (!apiKey && !endpoint) {
|
||||||
|
throw new Error('Please set either OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT');
|
||||||
}
|
}
|
||||||
this._apiKey = apiKey;
|
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(
|
public async apply(
|
||||||
@ -224,19 +255,25 @@ export class OpenAIAssistant implements Assistant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchCompletion(messages: AssistanceMessage[], userIdHash: string, longerContext: boolean) {
|
private async _fetchCompletion(messages: AssistanceMessage[], userIdHash: string, longerContext: boolean) {
|
||||||
|
const model = longerContext ? this._longerContextModel : this._model;
|
||||||
const apiResponse = await DEPS.fetch(
|
const apiResponse = await DEPS.fetch(
|
||||||
this._endpoint,
|
this._endpoint,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
...(this._apiKey ? {
|
||||||
"Authorization": `Bearer ${this._apiKey}`,
|
"Authorization": `Bearer ${this._apiKey}`,
|
||||||
|
} : undefined),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages,
|
messages,
|
||||||
temperature: 0,
|
temperature: 0,
|
||||||
model: longerContext ? OpenAIAssistant.LONGER_CONTEXT_MODEL : OpenAIAssistant.DEFAULT_MODEL,
|
...(model ? { model } : undefined),
|
||||||
user: userIdHash,
|
user: userIdHash,
|
||||||
|
...(this._maxTokens ? {
|
||||||
|
max_tokens: this._maxTokens,
|
||||||
|
} : undefined),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -244,7 +281,7 @@ export class OpenAIAssistant implements Assistant {
|
|||||||
const result = JSON.parse(resultText);
|
const result = JSON.parse(resultText);
|
||||||
const errorCode = result.error?.code;
|
const errorCode = result.error?.code;
|
||||||
if (errorCode === "context_length_exceeded" || result.choices?.[0].finish_reason === "length") {
|
if (errorCode === "context_length_exceeded" || result.choices?.[0].finish_reason === "length") {
|
||||||
if (!longerContext) {
|
if (!longerContext && this._longerContextModel) {
|
||||||
log.info("Switching to longer context model...");
|
log.info("Switching to longer context model...");
|
||||||
throw new SwitchToLongerContext();
|
throw new SwitchToLongerContext();
|
||||||
} else if (messages.length <= 2) {
|
} else if (messages.length <= 2) {
|
||||||
@ -394,14 +431,10 @@ export function getAssistant() {
|
|||||||
if (process.env.OPENAI_API_KEY === 'test') {
|
if (process.env.OPENAI_API_KEY === 'test') {
|
||||||
return new EchoAssistant();
|
return new EchoAssistant();
|
||||||
}
|
}
|
||||||
if (process.env.OPENAI_API_KEY) {
|
if (process.env.OPENAI_API_KEY || process.env.ASSISTANT_CHAT_COMPLETION_ENDPOINT) {
|
||||||
return new OpenAIAssistant();
|
return new OpenAIAssistant();
|
||||||
}
|
}
|
||||||
// Maintaining this is too much of a burden for now.
|
throw new Error('Please set OPENAI_API_KEY or ASSISTANT_CHAT_COMPLETION_ENDPOINT');
|
||||||
// if (process.env.HUGGINGFACE_API_KEY) {
|
|
||||||
// return new HuggingFaceAssistant();
|
|
||||||
// }
|
|
||||||
throw new Error('Please set OPENAI_API_KEY');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +74,8 @@ export function makeGristConfig(options: MakeGristConfigOptons): GristLoadConfig
|
|||||||
supportedLngs: readLoadedLngs(req?.i18n),
|
supportedLngs: readLoadedLngs(req?.i18n),
|
||||||
namespaces: readLoadedNamespaces(req?.i18n),
|
namespaces: readLoadedNamespaces(req?.i18n),
|
||||||
featureComments: isAffirmative(process.env.COMMENTS),
|
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,
|
supportEmail: SUPPORT_EMAIL,
|
||||||
userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale,
|
userLocale: (req as RequestWithLogin | undefined)?.user?.options?.locale,
|
||||||
telemetry: server?.getTelemetry().getTelemetryConfig(),
|
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.",
|
"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",
|
"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:",
|
"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": {
|
"GridView": {
|
||||||
"Click to insert": "Haga clic para insertar"
|
"Click to insert": "Haga clic para insertar"
|
||||||
|
@ -830,7 +830,9 @@
|
|||||||
"Cell Style": "Style de cellule",
|
"Cell Style": "Style de cellule",
|
||||||
"CELL STYLE": "STYLE de CELLULE",
|
"CELL STYLE": "STYLE de CELLULE",
|
||||||
"Default cell style": "Style par défaut",
|
"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": {
|
"DiscussionEditor": {
|
||||||
"Comment": "Commentaire",
|
"Comment": "Commentaire",
|
||||||
|
@ -67,7 +67,22 @@
|
|||||||
"Importer": {
|
"Importer": {
|
||||||
"Update existing records": "Aggiorna i record esistenti",
|
"Update existing records": "Aggiorna i record esistenti",
|
||||||
"Merge rows that match these fields:": "Unisci le righe che corrispondono a questi campi:",
|
"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": {
|
"NotifyUI": {
|
||||||
"Cannot find personal site, sorry!": "Spiacente, impossibile trovare il sito personale!",
|
"Cannot find personal site, sorry!": "Spiacente, impossibile trovare il sito personale!",
|
||||||
@ -236,7 +251,10 @@
|
|||||||
"CELL STYLE": "STILE CELLA",
|
"CELL STYLE": "STILE CELLA",
|
||||||
"Mixed style": "Stile misto",
|
"Mixed style": "Stile misto",
|
||||||
"Open row styles": "Apri stili riga",
|
"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": {
|
"ChoiceTextBox": {
|
||||||
"CHOICES": "SCELTE"
|
"CHOICES": "SCELTE"
|
||||||
@ -1059,7 +1077,8 @@
|
|||||||
"Sign Up for Free": "Iscriviti gratis",
|
"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:",
|
"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?",
|
"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": {
|
"GridView": {
|
||||||
"Click to insert": "Clicca per inserire"
|
"Click to insert": "Clicca per inserire"
|
||||||
|
@ -183,7 +183,7 @@
|
|||||||
"Activation": "Активация",
|
"Activation": "Активация",
|
||||||
"Billing Account": "Расчетный счет",
|
"Billing Account": "Расчетный счет",
|
||||||
"Sign In": "Войти",
|
"Sign In": "Войти",
|
||||||
"Sign Up": "Подписаться",
|
"Sign Up": "Зарегистрироваться",
|
||||||
"Use This Template": "Использовать этот шаблон"
|
"Use This Template": "Использовать этот шаблон"
|
||||||
},
|
},
|
||||||
"ActionLog": {
|
"ActionLog": {
|
||||||
|
@ -287,8 +287,8 @@ describe('gristUrlState', function() {
|
|||||||
it('should support an update function to pushUrl and makeUrl', async 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;
|
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>;
|
const state = UrlState.create(null, mockWindow, prod) as UrlState<IGristUrlState>;
|
||||||
await state.pushUrl({params: {style: 'light', linkParameters: {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=light&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
|
state.loadState(); // changing linkParameters requires a page reload
|
||||||
assert.equal(state.makeUrl((prevState) => merge({}, prevState, {params: {style: 'full'}})),
|
assert.equal(state.makeUrl((prevState) => merge({}, prevState, {params: {style: 'full'}})),
|
||||||
'https://bar.example.com/doc/DOC/p/5?style=full&foo_=A&bar_=B');
|
'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';
|
import {assert} from 'chai';
|
||||||
|
|
||||||
describe('gristUrls', function() {
|
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() {
|
describe('parseFirstUrlPart', function() {
|
||||||
it('should strip out matching tag', function() {
|
it('should strip out matching tag', function() {
|
||||||
assert.deepEqual(parseFirstUrlPart('o', '/o/foo/bar?x#y'), {value: 'foo', path: '/bar?x#y'});
|
assert.deepEqual(parseFirstUrlPart('o', '/o/foo/bar?x#y'), {value: 'foo', path: '/bar?x#y'});
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
*
|
*
|
||||||
* USAGE:
|
* USAGE:
|
||||||
* OPENAI_API_KEY=<my_openai_api_key> node core/test/formula-dataset/runCompletion.js
|
* 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:
|
* # WITH VERBOSE:
|
||||||
* VERBOSE=1 OPENAI_API_KEY=<my_openai_api_key> node core/test/formula-dataset/runCompletion.js
|
* 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;
|
const FOLLOWUP_EVALUATE = false;
|
||||||
|
|
||||||
export async function runCompletion() {
|
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 template directory not exists, make it
|
||||||
if (!fs.existsSync(path.join(PATH_TO_DOC))) {
|
if (!fs.existsSync(path.join(PATH_TO_DOC))) {
|
||||||
|
@ -675,7 +675,7 @@ async function openMenu(tableId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function waitForRawData() {
|
async function waitForRawData() {
|
||||||
await driver.findWait('.test-raw-data-list', 1000);
|
await driver.findWait('.test-raw-data-list', 2000);
|
||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user