feat: add AI settings configuration

- Add AI settings protobuf definitions and workspace service
- Implement AI settings management in backend store
- Add AI settings UI component with localization support
- Integrate AI settings into workspace and settings page

Signed-off-by: Chao Liu <chaoliu719@gmail.com>
This commit is contained in:
Chao Liu 2025-08-16 13:56:32 +08:00 committed by ChaoLiu
parent f300b9499e
commit 1a58d1c633
9 changed files with 452 additions and 7 deletions

View File

@ -68,6 +68,7 @@ message WorkspaceSetting {
GeneralSetting general_setting = 2;
StorageSetting storage_setting = 3;
MemoRelatedSetting memo_related_setting = 4;
AiSetting ai_setting = 5;
}
// Enumeration of workspace setting keys.
@ -79,6 +80,8 @@ message WorkspaceSetting {
STORAGE = 2;
// MEMO_RELATED is the key for memo related settings.
MEMO_RELATED = 3;
// AI is the key for AI settings.
AI = 4;
}
// General workspace settings configuration.
@ -170,6 +173,20 @@ message WorkspaceSetting {
// nsfw_tags is the list of tags that mark content as NSFW for blurring.
repeated string nsfw_tags = 10;
}
// AI configuration settings for workspace AI features.
message AiSetting {
// enable_ai enables AI features.
bool enable_ai = 1;
// base_url is the base URL for AI API.
string base_url = 2;
// api_key is the API key for AI service.
string api_key = 3;
// model is the AI model to use.
string model = 4;
// timeout_seconds is the timeout for AI requests in seconds.
int32 timeout_seconds = 5;
}
}
// Request message for GetWorkspaceSetting method.

View File

@ -14,6 +14,8 @@ enum WorkspaceSettingKey {
STORAGE = 3;
// MEMO_RELATED is the key for memo related settings.
MEMO_RELATED = 4;
// AI is the key for AI settings.
AI = 5;
}
message WorkspaceSetting {
@ -23,6 +25,7 @@ message WorkspaceSetting {
WorkspaceGeneralSetting general_setting = 3;
WorkspaceStorageSetting storage_setting = 4;
WorkspaceMemoRelatedSetting memo_related_setting = 5;
WorkspaceAISetting ai_setting = 6;
}
}
@ -115,3 +118,16 @@ message WorkspaceMemoRelatedSetting {
// nsfw_tags is the list of tags that mark content as NSFW for blurring.
repeated string nsfw_tags = 10;
}
message WorkspaceAISetting {
// enable_ai enables AI features.
bool enable_ai = 1;
// base_url is the base URL for AI API.
string base_url = 2;
// api_key is the API key for AI service.
string api_key = 3;
// model is the AI model to use.
string model = 4;
// timeout_seconds is the timeout for AI requests in seconds.
int32 timeout_seconds = 5;
}

View File

@ -47,6 +47,8 @@ func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, request *v1pb.Ge
_, err = s.Store.GetWorkspaceMemoRelatedSetting(ctx)
case storepb.WorkspaceSettingKey_STORAGE:
_, err = s.Store.GetWorkspaceStorageSetting(ctx)
case storepb.WorkspaceSettingKey_AI:
_, err = s.Store.GetWorkspaceAISetting(ctx)
default:
return nil, status.Errorf(codes.InvalidArgument, "unsupported workspace setting key: %v", workspaceSettingKey)
}
@ -64,8 +66,8 @@ func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, request *v1pb.Ge
return nil, status.Errorf(codes.NotFound, "workspace setting not found")
}
// For storage setting, only host can get it.
if workspaceSetting.Key == storepb.WorkspaceSettingKey_STORAGE {
// For storage and AI settings, only host can get them.
if workspaceSetting.Key == storepb.WorkspaceSettingKey_STORAGE || workspaceSetting.Key == storepb.WorkspaceSettingKey_AI {
user, err := s.GetCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
@ -116,6 +118,10 @@ func convertWorkspaceSettingFromStore(setting *storepb.WorkspaceSetting) *v1pb.W
workspaceSetting.Value = &v1pb.WorkspaceSetting_MemoRelatedSetting_{
MemoRelatedSetting: convertWorkspaceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()),
}
case *storepb.WorkspaceSetting_AiSetting:
workspaceSetting.Value = &v1pb.WorkspaceSetting_AiSetting_{
AiSetting: convertWorkspaceAISettingFromStore(setting.GetAiSetting()),
}
}
return workspaceSetting
}
@ -141,6 +147,10 @@ func convertWorkspaceSettingToStore(setting *v1pb.WorkspaceSetting) *storepb.Wor
workspaceSetting.Value = &storepb.WorkspaceSetting_MemoRelatedSetting{
MemoRelatedSetting: convertWorkspaceMemoRelatedSettingToStore(setting.GetMemoRelatedSetting()),
}
case storepb.WorkspaceSettingKey_AI:
workspaceSetting.Value = &storepb.WorkspaceSetting_AiSetting{
AiSetting: convertWorkspaceAISettingToStore(setting.GetAiSetting()),
}
}
return workspaceSetting
}
@ -279,6 +289,50 @@ func convertWorkspaceMemoRelatedSettingToStore(setting *v1pb.WorkspaceSetting_Me
}
}
func convertWorkspaceAISettingFromStore(setting *storepb.WorkspaceAISetting) *v1pb.WorkspaceSetting_AiSetting {
if setting == nil {
return &v1pb.WorkspaceSetting_AiSetting{
EnableAi: false,
BaseUrl: "",
ApiKey: "",
Model: "",
TimeoutSeconds: 10,
}
}
result := &v1pb.WorkspaceSetting_AiSetting{
EnableAi: setting.EnableAi,
BaseUrl: setting.BaseUrl,
ApiKey: setting.ApiKey,
Model: setting.Model,
TimeoutSeconds: setting.TimeoutSeconds,
}
return result
}
func convertWorkspaceAISettingToStore(setting *v1pb.WorkspaceSetting_AiSetting) *storepb.WorkspaceAISetting {
if setting == nil {
return &storepb.WorkspaceAISetting{
EnableAi: false,
BaseUrl: "",
ApiKey: "",
Model: "",
TimeoutSeconds: 10,
}
}
result := &storepb.WorkspaceAISetting{
EnableAi: setting.EnableAi,
BaseUrl: setting.BaseUrl,
ApiKey: setting.ApiKey,
Model: setting.Model,
TimeoutSeconds: setting.TimeoutSeconds,
}
return result
}
var ownerCache *v1pb.User
func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error) {

View File

@ -2,6 +2,8 @@ package store
import (
"context"
"os"
"strconv"
"github.com/pkg/errors"
"google.golang.org/protobuf/encoding/protojson"
@ -37,6 +39,8 @@ func (s *Store) UpsertWorkspaceSetting(ctx context.Context, upsert *storepb.Work
valueBytes, err = protojson.Marshal(upsert.GetStorageSetting())
} else if upsert.Key == storepb.WorkspaceSettingKey_MEMO_RELATED {
valueBytes, err = protojson.Marshal(upsert.GetMemoRelatedSetting())
} else if upsert.Key == storepb.WorkspaceSettingKey_AI {
valueBytes, err = protojson.Marshal(upsert.GetAiSetting())
} else {
return nil, errors.Errorf("unsupported workspace setting key: %v", upsert.Key)
}
@ -208,6 +212,64 @@ func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.Worksp
return workspaceStorageSetting, nil
}
const (
defaultAIEnabled = false
defaultAITimeoutSeconds = int32(15)
)
func (s *Store) GetWorkspaceAISetting(ctx context.Context) (*storepb.WorkspaceAISetting, error) {
workspaceSetting, err := s.GetWorkspaceSetting(ctx, &FindWorkspaceSetting{
Name: storepb.WorkspaceSettingKey_AI.String(),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace AI setting")
}
workspaceAISetting := &storepb.WorkspaceAISetting{}
if workspaceSetting != nil {
workspaceAISetting = workspaceSetting.GetAiSetting()
} else {
// If no database setting exists, use environment variables as defaults
workspaceAISetting = loadAISettingFromEnv()
}
// Set default timeout if not configured
if workspaceAISetting.TimeoutSeconds <= 0 {
workspaceAISetting.TimeoutSeconds = defaultAITimeoutSeconds
}
s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_AI.String(), &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey_AI,
Value: &storepb.WorkspaceSetting_AiSetting{AiSetting: workspaceAISetting},
})
return workspaceAISetting, nil
}
// loadAISettingFromEnv loads AI configuration from environment variables
func loadAISettingFromEnv() *storepb.WorkspaceAISetting {
timeoutSeconds := defaultAITimeoutSeconds
if timeoutStr := os.Getenv("AI_TIMEOUT_SECONDS"); timeoutStr != "" {
if timeout, err := strconv.Atoi(timeoutStr); err == nil && timeout > 0 {
timeoutSeconds = int32(timeout)
}
}
baseURL := os.Getenv("AI_BASE_URL")
apiKey := os.Getenv("AI_API_KEY")
model := os.Getenv("AI_MODEL")
// Enable AI if all required fields are provided via environment variables
enableAI := baseURL != "" && apiKey != "" && model != ""
return &storepb.WorkspaceAISetting{
EnableAi: enableAI,
BaseUrl: baseURL,
ApiKey: apiKey,
Model: model,
TimeoutSeconds: timeoutSeconds,
}
}
func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*storepb.WorkspaceSetting, error) {
workspaceSetting := &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingRaw.Name]),
@ -237,6 +299,12 @@ func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*sto
return nil, err
}
workspaceSetting.Value = &storepb.WorkspaceSetting_MemoRelatedSetting{MemoRelatedSetting: memoRelatedSetting}
case storepb.WorkspaceSettingKey_AI.String():
aiSetting := &storepb.WorkspaceAISetting{}
if err := protojsonUnmarshaler.Unmarshal([]byte(workspaceSettingRaw.Value), aiSetting); err != nil {
return nil, err
}
workspaceSetting.Value = &storepb.WorkspaceSetting_AiSetting{AiSetting: aiSetting}
default:
// Skip unsupported workspace setting key.
return nil, nil

View File

@ -0,0 +1,200 @@
import { isEqual } from "lodash-es";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { workspaceStore } from "@/store";
import { workspaceSettingNamePrefix } from "@/store/common";
import { WorkspaceSetting_AiSetting, WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
import { useTranslate } from "@/utils/i18n";
const AISettings = observer(() => {
const t = useTranslate();
const [originalSetting, setOriginalSetting] = useState<WorkspaceSetting_AiSetting>(workspaceStore.state.aiSetting);
const [aiSetting, setAiSetting] = useState<WorkspaceSetting_AiSetting>(originalSetting);
const [showApiKey, setShowApiKey] = useState(false);
const updatePartialSetting = (partial: Partial<WorkspaceSetting_AiSetting>) => {
setAiSetting(WorkspaceSetting_AiSetting.fromPartial({ ...aiSetting, ...partial }));
};
const saveSetting = async (settingToSave: WorkspaceSetting_AiSetting) => {
try {
await workspaceStore.updateWorkspaceSetting({
name: `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.AI}`,
aiSetting: settingToSave,
});
setOriginalSetting(settingToSave);
setAiSetting(settingToSave);
toast.success(t("message.update-succeed"));
} catch (error: any) {
console.error(error);
toast.error(error.response?.data?.message || error.message || t("message.update-failed"));
}
};
const updateEnableAI = async (enabled: boolean) => {
const newSetting = WorkspaceSetting_AiSetting.fromPartial({ ...aiSetting, enableAi: enabled });
await saveSetting(newSetting);
};
const updateSetting = async () => {
if (aiSetting.enableAi && (!aiSetting.apiKey || !aiSetting.model)) {
toast.error(t("setting.ai-section.api-key-model-required"));
return;
}
const settingToSave = WorkspaceSetting_AiSetting.fromPartial({
...aiSetting,
baseUrl: aiSetting.baseUrl || "https://api.openai.com/v1",
timeoutSeconds: aiSetting.timeoutSeconds || 10,
});
await saveSetting(settingToSave);
};
const resetSetting = () => setAiSetting(originalSetting);
// 只比较全局AI配置的变化不包括子功能配置
const globalSettingChanged = !isEqual(
{
enableAi: originalSetting.enableAi,
baseUrl: originalSetting.baseUrl,
apiKey: originalSetting.apiKey,
model: originalSetting.model,
timeoutSeconds: originalSetting.timeoutSeconds,
},
{
enableAi: aiSetting.enableAi,
baseUrl: aiSetting.baseUrl,
apiKey: aiSetting.apiKey,
model: aiSetting.model,
timeoutSeconds: aiSetting.timeoutSeconds,
},
);
return (
<div className="w-full flex flex-col gap-6 pt-2 pb-4">
{/* Global AI Settings */}
<div className="w-full flex flex-col gap-2">
<div className="w-full flex flex-row justify-between items-center">
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-gray-400">{t("setting.ai-section.title")}</span>
<Badge variant={aiSetting.enableAi ? "default" : "secondary"}>
{aiSetting.enableAi ? t("common.enabled") : t("common.disabled")}
</Badge>
</div>
</div>
<p className="text-sm text-gray-500">{t("setting.ai-section.description")}</p>
<div className="w-full flex flex-col gap-4 mt-4">
{/* Enable AI Toggle */}
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-col">
<Label htmlFor="enable-ai">{t("setting.ai-section.enable-ai")}</Label>
<span className="text-sm text-gray-500">{t("setting.ai-section.enable-ai-description")}</span>
</div>
<Switch id="enable-ai" checked={aiSetting.enableAi} onCheckedChange={updateEnableAI} />
</div>
{/* AI Global Configuration Fields */}
{aiSetting.enableAi && (
<>
<div className="w-full flex flex-col gap-2">
<Label htmlFor="base-url">{t("setting.ai-section.base-url")}</Label>
<Input
id="base-url"
type="url"
placeholder="https://api.openai.com/v1"
value={aiSetting.baseUrl}
onChange={(e) => updatePartialSetting({ baseUrl: e.target.value })}
/>
<span className="text-sm text-gray-500">{t("setting.ai-section.base-url-description")}</span>
</div>
<div className="w-full flex flex-col gap-2">
<Label htmlFor="api-key">{t("setting.ai-section.api-key")}</Label>
<div className="relative">
<Input
id="api-key"
type="text"
placeholder="sk-..."
value={aiSetting.apiKey}
onChange={(e) => updatePartialSetting({ apiKey: e.target.value })}
autoComplete="off"
style={
showApiKey
? {}
: ({
WebkitTextSecurity: "disc",
fontFamily: "text-security-disc, -webkit-small-control",
} as React.CSSProperties)
}
className="pr-10"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-1 top-1 h-7 w-7 p-0"
onClick={() => setShowApiKey(!showApiKey)}
>
{showApiKey ? <EyeOffIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
</Button>
</div>
<span className="text-sm text-gray-500">{t("setting.ai-section.api-key-description")}</span>
</div>
<div className="w-full flex flex-col gap-2">
<Label htmlFor="model">{t("setting.ai-section.model")}</Label>
<Input
id="model"
type="text"
placeholder="gpt-4o, claude-3-5-sonnet-20241022..."
value={aiSetting.model}
onChange={(e) => updatePartialSetting({ model: e.target.value })}
/>
<span className="text-sm text-gray-500">{t("setting.ai-section.model-description")}</span>
</div>
<div className="w-full flex flex-col gap-2">
<Label htmlFor="timeout">{t("setting.ai-section.timeout")}</Label>
<Input
id="timeout"
type="number"
min="5"
max="60"
placeholder="10"
value={aiSetting.timeoutSeconds}
onChange={(e) => updatePartialSetting({ timeoutSeconds: parseInt(e.target.value) || 10 })}
/>
<span className="text-sm text-gray-500">{t("setting.ai-section.timeout-description")}</span>
</div>
</>
)}
</div>
{/* Action Buttons */}
{aiSetting.enableAi && (
<div className="w-full flex flex-row justify-end items-center gap-2 mt-4">
<Button variant="outline" onClick={resetSetting} disabled={!globalSettingChanged}>
{t("common.cancel")}
</Button>
<Button onClick={updateSetting} disabled={!globalSettingChanged}>
{t("common.save")}
</Button>
</div>
)}
</div>
</div>
);
});
export default AISettings;

View File

@ -38,8 +38,10 @@
"days": "Days",
"delete": "Delete",
"description": "Description",
"disabled": "Disabled",
"edit": "Edit",
"email": "Email",
"enabled": "Enabled",
"expand": "Expand",
"explore": "Explore",
"file": "File",
@ -189,6 +191,7 @@
"restored-successfully": "Restored successfully",
"succeed-copy-link": "Link copied successfully.",
"update-succeed": "Update succeeded",
"update-failed": "Update failed",
"user-not-found": "User not found"
},
"reference": {
@ -304,11 +307,46 @@
},
"my-account": "My Account",
"preference": "Preferences",
"ai": "AI",
"preference-section": {
"default-memo-sort-option": "Memo display time",
"default-memo-visibility": "Default memo visibility",
"theme": "Theme"
},
"ai-section": {
"title": "AI Settings",
"description": "Configure AI services for enhanced features like tag recommendations.",
"enable-ai": "Enable AI Features",
"enable-ai-description": "Turn on AI-powered features throughout the application",
"base-url": "Base URL",
"base-url-description": "API endpoint for your AI service (e.g., https://api.openai.com/v1)",
"api-key": "API Key",
"api-key-description": "Authentication key for your AI service",
"model": "Model",
"model-description": "AI model to use for processing requests (e.g., gpt-4o, claude-3-5-sonnet-20241022)",
"timeout": "Timeout (seconds)",
"timeout-description": "Request timeout in seconds (5-60)",
"test-connection": "Test Connection",
"testing-connection": "Testing...",
"test-connection-description": "Verify that your AI service configuration is working",
"test-connection-success": "Connection successful! AI service is properly configured.",
"test-connection-failed": "Connection failed. Please check your configuration.",
"test-connection-incomplete": "Please fill in all required fields before testing.",
"fields-required": "All fields are required when AI is enabled.",
"api-key-model-required": "API key and model are required when AI is enabled."
},
"tag-recommendation": {
"title": "Tag Recommendation",
"description": "Use AI to recommend suitable tags for your memo content",
"enable": "Enable Tag Recommendation",
"enable-description": "Get AI-powered tag suggestions in the memo editor",
"system-prompt": "Custom System Prompt",
"system-prompt-placeholder": "Leave empty to use default prompt...",
"system-prompt-description": "Customize the AI instruction for tag recommendations. Leave empty to use the built-in default prompt",
"rate-limit": "Rate Limit (requests/minute)",
"rate-limit-description": "Maximum number of tag recommendation requests per minute"
},
"ai-features": "AI Features",
"sso": "SSO",
"sso-section": {
"authorization-endpoint": "Authorization endpoint",

View File

@ -37,8 +37,10 @@
"days": "天",
"delete": "删除",
"description": "说明",
"disabled": "已禁用",
"edit": "编辑",
"email": "邮箱",
"enabled": "已启用",
"expand": "展开",
"explore": "发现",
"file": "文件",
@ -303,11 +305,34 @@
},
"my-account": "我的账号",
"preference": "偏好设置",
"ai": "AI",
"preference-section": {
"default-memo-sort-option": "备忘录显示时间",
"default-memo-visibility": "默认备忘录可见性",
"theme": "主题"
},
"ai-section": {
"title": "AI 设置",
"description": "配置 AI 服务以启用标签推荐等增强功能。",
"enable-ai": "启用 AI 功能",
"enable-ai-description": "在应用程序中开启 AI 驱动的功能",
"base-url": "基础 URL",
"base-url-description": "AI 服务的 API 端点例如https://api.openai.com/v1",
"api-key": "API 密钥",
"api-key-description": "AI 服务的身份验证密钥",
"model": "模型",
"model-description": "用于处理请求的 AI 模型例如gpt-4o, claude-3-5-sonnet-20241022",
"timeout": "超时(秒)",
"timeout-description": "请求超时时间单位秒5-60",
"test-connection": "测试连接",
"testing-connection": "测试中...",
"test-connection-description": "验证您的 AI 服务配置是否正常工作",
"test-connection-success": "连接成功AI 服务配置正确。",
"test-connection-failed": "连接失败。请检查您的配置。",
"test-connection-incomplete": "请在测试前填写所有必填字段。",
"fields-required": "启用 AI 时,所有字段都是必填的。",
"api-key-model-required": "启用 AI 时API 密钥和模型是必填的。"
},
"sso": "单点登录",
"sso-section": {
"authorization-endpoint": "授权端点Authorization Endpoint",

View File

@ -1,8 +1,9 @@
import { CogIcon, DatabaseIcon, KeyIcon, LibraryIcon, LucideIcon, Settings2Icon, UserIcon, UsersIcon } from "lucide-react";
import { CogIcon, DatabaseIcon, KeyIcon, LibraryIcon, LucideIcon, Settings2Icon, SparklesIcon, UserIcon, UsersIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation } from "react-router-dom";
import MobileHeader from "@/components/MobileHeader";
import AISettings from "@/components/Settings/AISettings";
import MemberSection from "@/components/Settings/MemberSection";
import MemoRelatedSettings from "@/components/Settings/MemoRelatedSettings";
import MyAccountSection from "@/components/Settings/MyAccountSection";
@ -19,14 +20,14 @@ import { User_Role } from "@/types/proto/api/v1/user_service";
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
import { useTranslate } from "@/utils/i18n";
type SettingSection = "my-account" | "preference" | "member" | "system" | "memo-related" | "storage" | "sso";
type SettingSection = "my-account" | "preference" | "member" | "system" | "memo-related" | "storage" | "sso" | "ai";
interface State {
selectedSection: SettingSection;
}
const BASIC_SECTIONS: SettingSection[] = ["my-account", "preference"];
const ADMIN_SECTIONS: SettingSection[] = ["member", "system", "memo-related", "storage", "sso"];
const ADMIN_SECTIONS: SettingSection[] = ["member", "system", "memo-related", "storage", "ai", "sso"];
const SECTION_ICON_MAP: Record<SettingSection, LucideIcon> = {
"my-account": UserIcon,
preference: CogIcon,
@ -34,6 +35,7 @@ const SECTION_ICON_MAP: Record<SettingSection, LucideIcon> = {
system: Settings2Icon,
"memo-related": LibraryIcon,
storage: DatabaseIcon,
ai: SparklesIcon,
sso: KeyIcon,
};
@ -73,7 +75,7 @@ const Setting = observer(() => {
// Initial fetch for workspace settings.
(async () => {
[WorkspaceSetting_Key.MEMO_RELATED, WorkspaceSetting_Key.STORAGE].forEach(async (key) => {
[WorkspaceSetting_Key.MEMO_RELATED, WorkspaceSetting_Key.STORAGE, WorkspaceSetting_Key.AI].forEach(async (key) => {
await workspaceStore.fetchWorkspaceSetting(key);
});
})();
@ -148,6 +150,8 @@ const Setting = observer(() => {
<MemoRelatedSettings />
) : state.selectedSection === "storage" ? (
<StorageSection />
) : state.selectedSection === "ai" ? (
<AISettings />
) : state.selectedSection === "sso" ? (
<SSOSection />
) : null}

View File

@ -5,6 +5,7 @@ import { WorkspaceProfile, WorkspaceSetting_Key } from "@/types/proto/api/v1/wor
import {
WorkspaceSetting_GeneralSetting,
WorkspaceSetting_MemoRelatedSetting,
WorkspaceSetting_AiSetting,
WorkspaceSetting,
} from "@/types/proto/api/v1/workspace_service";
import { isValidateLocale } from "@/utils/i18n";
@ -30,6 +31,19 @@ class LocalState {
);
}
get aiSetting() {
return (
this.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${WorkspaceSetting_Key.AI}`)?.aiSetting ||
WorkspaceSetting_AiSetting.fromPartial({
enableAi: false,
baseUrl: "",
apiKey: "",
model: "",
timeoutSeconds: 10,
})
);
}
constructor() {
makeAutoObservable(this);
}
@ -66,6 +80,14 @@ const workspaceStore = (() => {
});
};
const updateWorkspaceSetting = async (setting: WorkspaceSetting) => {
const response = await workspaceServiceClient.updateWorkspaceSetting({ setting });
state.setPartial({
settings: uniqBy([response, ...state.settings], "name"),
});
return response;
};
const getWorkspaceSettingByKey = (settingKey: WorkspaceSetting_Key) => {
return (
state.settings.find((setting) => setting.name === `${workspaceSettingNamePrefix}${settingKey}`) || WorkspaceSetting.fromPartial({})
@ -96,6 +118,7 @@ const workspaceStore = (() => {
state,
fetchWorkspaceSetting,
upsertWorkspaceSetting,
updateWorkspaceSetting,
getWorkspaceSettingByKey,
setTheme,
};
@ -104,7 +127,7 @@ const workspaceStore = (() => {
export const initialWorkspaceStore = async () => {
const workspaceProfile = await workspaceServiceClient.getWorkspaceProfile({});
// Prepare workspace settings.
for (const key of [WorkspaceSetting_Key.GENERAL, WorkspaceSetting_Key.MEMO_RELATED]) {
for (const key of [WorkspaceSetting_Key.GENERAL, WorkspaceSetting_Key.MEMO_RELATED, WorkspaceSetting_Key.AI]) {
await workspaceStore.fetchWorkspaceSetting(key);
}