mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) updates from grist-core
This commit is contained in:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user