refactor(canvas): 替换自定义ID生成函数为nanoid并优化配置逻辑
- 移除自定义createId函数,统一使用nanoid库生成唯一标识符 - 将useEffectiveAiConfig Hook替换为resolveEffectiveConfig工具函数 - 直接从config store获取publicSettings避免额外的Hook依赖 - 更新所有组件中的ID生成调用以使用nanoid - 简化buildApiUrl函数中的基础URL规范化逻辑
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
- API 请求统一放在 `web/src/services/api/`。
|
||||
- 全局或跨页面状态优先放在 `web/src/stores/`。
|
||||
- 已经放在全局 store 或全局 hook 中的状态/动作,组件需要时直接使用对应 store/hook,不要为了“纯组件”层层透传 props;避免一个组件传递过多参数。
|
||||
- 全局组件、全局常量、全局配置等全局性质的内容不要作为 props 或参数层层传递;哪里需要就在哪里直接从对应全局入口获取。
|
||||
- 多个页面重复出现的 UI 副作用动作,例如复制文本并提示、下载并提示、统一确认弹窗,优先抽成 `web/src/hooks/` 下的全局 hook;不要放进 store,除非它确实是需要共享/订阅的状态。
|
||||
- 画布相关状态和组件放在 `web/src/app/(user)/canvas/` 内部。
|
||||
- 页面里只有一个主业务组件时直接写在 `page.tsx`,不要单独拆 `Manager` 组件再传一堆 props。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { loadEnvConfig } from "@next/env";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { parseChangelog } from "./src/lib/release-info";
|
||||
import { parseChangelog } from "@/lib/release";
|
||||
|
||||
const webDir = dirname(fileURLToPath(import.meta.url));
|
||||
const localVersion = readFileSync(resolve(webDir, "../VERSION"), "utf8").trim() || "dev";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useParams, useRouter } from "next/navigation";
|
||||
import { Home, ImageIcon, Images, Keyboard, List, LogOut, Menu, MessageSquare, Plus, Redo2, Settings2, Trash2, Undo2, Upload } from "lucide-react";
|
||||
|
||||
import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image";
|
||||
import { defaultConfig, isAiConfigReady, resolveEffectiveConfig, type AiConfig, useConfigStore } from "@/stores/use-config-store";
|
||||
import { defaultConfig, type AiConfig, useConfigStore, useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { resolveImageUrl, uploadImage, type UploadedImage } from "@/services/image-storage";
|
||||
import { nanoid } from "nanoid";
|
||||
import { getDataUrlByteSize, readImageMeta } from "@/lib/image-utils";
|
||||
@@ -231,7 +231,8 @@ function InfiniteCanvasPage() {
|
||||
});
|
||||
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useConfigStore((state) => resolveEffectiveConfig(state.config, state.publicSettings?.modelChannel || null));
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const addAsset = useAssetStore((state) => state.addAsset);
|
||||
const cleanupAssetImages = useAssetStore((state) => state.cleanupImages);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { motion } from "motion/react";
|
||||
|
||||
import { ImageGenerationPending } from "@/components/image-generation-pending";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
import { isAiConfigReady, resolveEffectiveConfig, useConfigStore, type AiConfig } from "@/stores/use-config-store";
|
||||
import { useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { nanoid } from "nanoid";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -41,9 +41,10 @@ type CanvasAssistantPanelProps = {
|
||||
export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeSessionId, onSelectNodeIds, onSessionsChange, onInsertImage, onInsertText, onPasteImage, onCollapseStart, onCollapse }: CanvasAssistantPanelProps) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useConfigStore((state) => resolveEffectiveConfig(state.config, state.publicSettings?.modelChannel || null));
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const cleanupImages = useAssetStore((state) => state.cleanupImages);
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const [width, setWidth] = useState(390);
|
||||
const [view, setView] = useState<"chat" | "history">("chat");
|
||||
|
||||
@@ -8,7 +8,7 @@ import localforage from "localforage";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
import { PromptSelectDialog } from "@/components/prompts/prompt-select-dialog";
|
||||
import { AssetPickerModal, type InsertAssetPayload } from "@/app/(user)/canvas/components/asset-picker-modal";
|
||||
import { isAiConfigReady, resolveEffectiveConfig, useConfigStore, type AiConfig } from "@/stores/use-config-store";
|
||||
import { useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { nanoid } from "nanoid";
|
||||
import { formatBytes, formatDuration, getDataUrlByteSize, readImageMeta } from "@/lib/image-utils";
|
||||
import { requestEdit, requestGeneration } from "@/services/api/image";
|
||||
@@ -60,8 +60,9 @@ export default function ImagePage() {
|
||||
const { message } = App.useApp();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useConfigStore((state) => resolveEffectiveConfig(state.config, state.publicSettings?.modelChannel || null));
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
const isAiConfigReady = useConfigStore((state) => state.isAiConfigReady);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
const addAsset = useAssetStore((state) => state.addAsset);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
||||
@@ -9,7 +9,7 @@ import { GitHubLink } from "@/components/github-link";
|
||||
import { UserStatusActions } from "@/components/user-status-actions";
|
||||
import { AnimatedThemeToggler } from "@/components/ui/animated-theme-toggler";
|
||||
import { VersionReleaseModal } from "@/components/version-release-modal";
|
||||
import { useConfigStore, type AiConfig } from "@/stores/use-config-store";
|
||||
import { useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
import { navigationTools, type NavigationToolSlug } from "@/lib/navigation-tools";
|
||||
import { fetchImageModels } from "@/services/api/image";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
@@ -28,7 +28,6 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
|
||||
const { message } = App.useApp();
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
const isConfigOpen = useConfigStore((state) => state.isConfigOpen);
|
||||
const shouldPromptContinue = useConfigStore((state) => state.shouldPromptContinue);
|
||||
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
|
||||
@@ -40,16 +39,11 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
|
||||
const isReady = useUserStore((state) => state.isReady);
|
||||
const logout = useUserStore((state) => state.clearSession);
|
||||
const publicSettings = useConfigStore((state) => state.publicSettings);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const modelChannel = publicSettings?.modelChannel;
|
||||
const allowCustomChannel = modelChannel?.allowCustomChannel === true;
|
||||
const effectiveMode = allowCustomChannel ? config.channelMode : "remote";
|
||||
const modelConfig = effectiveMode === "remote" && modelChannel ? {
|
||||
...config,
|
||||
models: modelChannel.availableModels,
|
||||
imageModel: modelChannel.availableModels.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
|
||||
textModel: modelChannel.availableModels.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
|
||||
systemPrompt: modelChannel.systemPrompt,
|
||||
} : config;
|
||||
const modelConfig = effectiveMode === "remote" ? effectiveConfig : config;
|
||||
|
||||
const finishConfig = () => {
|
||||
setConfigDialogOpen(false);
|
||||
@@ -163,7 +157,7 @@ export function AppTopNav({ activeToolSlug, config, onConfigChange, hideHeader =
|
||||
aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
/>
|
||||
<VersionReleaseModal currentVersion={appVersion} />
|
||||
<VersionReleaseModal />
|
||||
<GitHubLink />
|
||||
<Link href="/login" className="text-sm font-medium text-stone-600 underline-offset-4 transition hover:text-stone-950 hover:underline dark:text-stone-300 dark:hover:text-stone-100">
|
||||
登录
|
||||
|
||||
@@ -52,7 +52,6 @@ export function UserStatusActions({
|
||||
const theme = useThemeStore((state) => state.theme);
|
||||
const setTheme = useThemeStore((state) => state.setTheme);
|
||||
const storedUserName = useUserStore((state) => state.user?.username);
|
||||
const appVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
const resolvedUserName = userName || storedUserName;
|
||||
const avatarText = initial || (resolvedUserName?.trim()[0] || "U").toUpperCase();
|
||||
const naturalIconClass = "inline-flex size-8 shrink-0 items-center justify-center text-stone-600 transition hover:text-stone-950 dark:text-stone-300 dark:hover:text-white [&_svg]:size-4";
|
||||
@@ -79,7 +78,7 @@ export function UserStatusActions({
|
||||
aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
title={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"}
|
||||
/>
|
||||
<VersionReleaseModal currentVersion={appVersion} style={versionStyle} />
|
||||
<VersionReleaseModal style={versionStyle} />
|
||||
<GitHubLink className={cn("bg-transparent hover:bg-transparent dark:hover:bg-transparent", gitHubClassName)} style={gitHubStyle} />
|
||||
<div ref={accountRef}>
|
||||
<Dropdown
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { App, Modal, Tag, Timeline } from "antd";
|
||||
import { Modal, Tag, Timeline } from "antd";
|
||||
import { useVersionCheck } from "@/hooks/use-version-check";
|
||||
import { APP_VERSION } from "@/constant/env";
|
||||
|
||||
function getTagColor(type: string) {
|
||||
if (type === "新增") return "green";
|
||||
@@ -18,28 +18,12 @@ function getReleaseTitle(version: string) {
|
||||
}
|
||||
|
||||
type VersionReleaseModalProps = {
|
||||
currentVersion: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function VersionReleaseModal({ currentVersion, className, style }: VersionReleaseModalProps) {
|
||||
const { message } = App.useApp();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { latestVersion, releases, checking, hasNewVersion, checkLatestRelease } = useVersionCheck(currentVersion);
|
||||
|
||||
const handleCheckLatestRelease = useCallback((showMessage = false) => {
|
||||
void checkLatestRelease().then((success) => {
|
||||
if (!showMessage) return;
|
||||
if (success) message.success("已获取最新版本信息");
|
||||
else message.error("获取最新版本信息失败");
|
||||
});
|
||||
}, [checkLatestRelease, message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
handleCheckLatestRelease();
|
||||
}, [handleCheckLatestRelease, open]);
|
||||
export function VersionReleaseModal({ className, style }: VersionReleaseModalProps) {
|
||||
const { open, setOpen, openReleaseModal, latestVersion, releases, checking, hasNewVersion, checkLatestRelease } = useVersionCheck();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -47,11 +31,11 @@ export function VersionReleaseModal({ currentVersion, className, style }: Versio
|
||||
type="button"
|
||||
className={className || "shrink-0 cursor-pointer text-xs font-medium text-stone-500 transition hover:text-stone-950 dark:text-stone-400 dark:hover:text-white"}
|
||||
style={style}
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={openReleaseModal}
|
||||
title="查看版本更新"
|
||||
>
|
||||
<span className="relative inline-flex">
|
||||
{currentVersion}
|
||||
{APP_VERSION}
|
||||
{hasNewVersion ? <span className="absolute -right-1.5 -top-1 size-1.5 rounded-full bg-green-500" /> : null}
|
||||
</span>
|
||||
</button>
|
||||
@@ -66,7 +50,7 @@ export function VersionReleaseModal({ currentVersion, className, style }: Versio
|
||||
<div className="mb-5 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg border border-stone-200 p-3 dark:border-stone-800">
|
||||
<div className="text-xs text-stone-500 dark:text-stone-400">当前版本</div>
|
||||
<div className="mt-1 text-base font-semibold text-stone-950 dark:text-stone-100">{currentVersion}</div>
|
||||
<div className="mt-1 text-base font-semibold text-stone-950 dark:text-stone-100">{APP_VERSION}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-stone-200 p-3 dark:border-stone-800">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
@@ -74,7 +58,7 @@ export function VersionReleaseModal({ currentVersion, className, style }: Versio
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer bg-transparent p-0 text-[11px] font-normal text-stone-400 underline-offset-2 transition hover:text-stone-700 hover:underline dark:text-stone-500 dark:hover:text-stone-300"
|
||||
onClick={() => handleCheckLatestRelease(true)}
|
||||
onClick={() => void checkLatestRelease(true)}
|
||||
>
|
||||
{checking ? "检查中..." : "检查更新"}
|
||||
</button>
|
||||
@@ -92,7 +76,7 @@ export function VersionReleaseModal({ currentVersion, className, style }: Versio
|
||||
<span className="text-xs text-stone-500 dark:text-stone-400">{release.date}</span>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
{release.version === latestVersion ? <Tag color="green">最新</Tag> : null}
|
||||
{release.version === currentVersion ? <Tag>当前</Tag> : null}
|
||||
{release.version === APP_VERSION ? <Tag>当前</Tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1.5">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { parseChangelog, type ReleaseInfo } from "@/lib/release-info";
|
||||
import { App } from "antd";
|
||||
import { APP_VERSION } from "@/constant/env";
|
||||
import { parseChangelog, type ReleaseInfo } from "@/lib/release";
|
||||
|
||||
const latestVersionUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/VERSION";
|
||||
const latestChangelogUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/CHANGELOG.md";
|
||||
@@ -25,11 +26,14 @@ function isNewerVersion(latestVersion: string, currentVersion: string) {
|
||||
return latest.some((value, index) => value > current[index] && latest.slice(0, index).every((part, prevIndex) => part === current[prevIndex]));
|
||||
}
|
||||
|
||||
export function useVersionCheck(currentVersion: string) {
|
||||
export function useVersionCheck() {
|
||||
const currentVersion = APP_VERSION;
|
||||
const { message } = App.useApp();
|
||||
const localReleases = useMemo(readLocalReleases, []);
|
||||
const [latestVersion, setLatestVersion] = useState(currentVersion);
|
||||
const [releases, setReleases] = useState<ReleaseInfo[]>(localReleases);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasNewVersion = isNewerVersion(latestVersion, currentVersion);
|
||||
|
||||
const checkLatestVersion = useCallback(async () => {
|
||||
@@ -44,7 +48,7 @@ export function useVersionCheck(currentVersion: string) {
|
||||
}
|
||||
}, [currentVersion]);
|
||||
|
||||
const checkLatestRelease = useCallback(async () => {
|
||||
const checkLatestRelease = useCallback(async (showMessage = false) => {
|
||||
setChecking(true);
|
||||
try {
|
||||
const [versionResponse, changelogResponse] = await Promise.all([
|
||||
@@ -56,19 +60,35 @@ export function useVersionCheck(currentVersion: string) {
|
||||
const [version, changelog] = await Promise.all([versionResponse.text(), changelogResponse.text()]);
|
||||
setLatestVersion(version.trim() || currentVersion);
|
||||
if (changelog.trim()) setReleases(parseChangelog(changelog));
|
||||
if (showMessage) message.success("已获取最新版本信息");
|
||||
return true;
|
||||
} catch {
|
||||
setLatestVersion(currentVersion);
|
||||
setReleases(localReleases);
|
||||
if (showMessage) message.error("获取最新版本信息失败");
|
||||
return false;
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}, [currentVersion, localReleases]);
|
||||
}, [currentVersion, localReleases, message]);
|
||||
|
||||
useEffect(() => {
|
||||
void checkLatestVersion();
|
||||
}, [checkLatestVersion]);
|
||||
|
||||
return { latestVersion, releases, checking, hasNewVersion, checkLatestRelease };
|
||||
const openReleaseModal = useCallback(() => {
|
||||
setOpen(true);
|
||||
void checkLatestRelease();
|
||||
}, [checkLatestRelease]);
|
||||
|
||||
return {
|
||||
open,
|
||||
setOpen,
|
||||
openReleaseModal,
|
||||
latestVersion,
|
||||
releases,
|
||||
checking,
|
||||
hasNewVersion,
|
||||
checkLatestRelease,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
@@ -44,11 +45,31 @@ type ConfigStore = {
|
||||
shouldPromptContinue: boolean;
|
||||
updateConfig: <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
|
||||
loadPublicSettings: () => Promise<void>;
|
||||
isAiConfigReady: (config: AiConfig, model: string) => boolean;
|
||||
openConfigDialog: (shouldPromptContinue?: boolean) => void;
|
||||
setConfigDialogOpen: (isOpen: boolean) => void;
|
||||
clearPromptContinue: () => void;
|
||||
};
|
||||
|
||||
function resolveEffectiveConfig(config: AiConfig, modelChannel: AdminPublicSettings["modelChannel"] | null) {
|
||||
const channelMode = modelChannel?.allowCustomChannel ? config.channelMode : "remote";
|
||||
if (channelMode === "local" || !modelChannel) return { ...config, channelMode };
|
||||
const models = modelChannel.availableModels;
|
||||
return {
|
||||
...config,
|
||||
channelMode,
|
||||
models,
|
||||
model: models.includes(config.model) ? config.model : modelChannel.defaultModel,
|
||||
imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
|
||||
textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
|
||||
systemPrompt: modelChannel.systemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
function isAiConfigReady(config: AiConfig, model: string) {
|
||||
return Boolean(model.trim()) && (config.channelMode === "remote" || Boolean(config.baseUrl.trim() && config.apiKey.trim()));
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -73,6 +94,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
set({ isPublicSettingsLoading: false });
|
||||
}
|
||||
},
|
||||
isAiConfigReady: (config, model) => isAiConfigReady(config, model),
|
||||
openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }),
|
||||
setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }),
|
||||
clearPromptContinue: () => set({ shouldPromptContinue: false }),
|
||||
@@ -88,19 +110,10 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
),
|
||||
);
|
||||
|
||||
export function resolveEffectiveConfig(config: AiConfig, modelChannel: AdminPublicSettings["modelChannel"] | null) {
|
||||
const channelMode = modelChannel?.allowCustomChannel ? config.channelMode : "remote";
|
||||
if (channelMode === "local" || !modelChannel) return { ...config, channelMode };
|
||||
const models = modelChannel.availableModels;
|
||||
return {
|
||||
...config,
|
||||
channelMode,
|
||||
models,
|
||||
model: models.includes(config.model) ? config.model : modelChannel.defaultModel,
|
||||
imageModel: models.includes(config.imageModel) ? config.imageModel : modelChannel.defaultImageModel || modelChannel.defaultModel,
|
||||
textModel: models.includes(config.textModel) ? config.textModel : modelChannel.defaultTextModel || modelChannel.defaultModel,
|
||||
systemPrompt: modelChannel.systemPrompt,
|
||||
};
|
||||
export function useEffectiveConfig() {
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const modelChannel = useConfigStore((state) => state.publicSettings?.modelChannel || null);
|
||||
return useMemo(() => resolveEffectiveConfig(config, modelChannel), [config, modelChannel]);
|
||||
}
|
||||
|
||||
export function buildApiUrl(baseUrl: string, path: string) {
|
||||
@@ -108,7 +121,3 @@ export function buildApiUrl(baseUrl: string, path: string) {
|
||||
const apiBaseUrl = normalizedBaseUrl.endsWith("/v1") ? normalizedBaseUrl : `${normalizedBaseUrl}/v1`;
|
||||
return `${apiBaseUrl}${path}`;
|
||||
}
|
||||
|
||||
export function isAiConfigReady(config: AiConfig, model: string) {
|
||||
return Boolean(model.trim()) && (config.channelMode === "remote" || Boolean(config.baseUrl.trim() && config.apiKey.trim()));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user