diff --git a/db/migrations-wstore/000008_aimeta.down.sql b/db/migrations-wstore/000008_aimeta.down.sql new file mode 100644 index 0000000000..b654758c26 --- /dev/null +++ b/db/migrations-wstore/000008_aimeta.down.sql @@ -0,0 +1 @@ +-- presets exist in config files, and should automatically prepopulate the meta in the older code versions \ No newline at end of file diff --git a/db/migrations-wstore/000008_aimeta.up.sql b/db/migrations-wstore/000008_aimeta.up.sql new file mode 100644 index 0000000000..203902ef99 --- /dev/null +++ b/db/migrations-wstore/000008_aimeta.up.sql @@ -0,0 +1,18 @@ +--- removes all ai: keys except ai:preset +UPDATE db_block +SET data = json_remove( + db_block.data, + '$.meta.ai:*', + '$.meta.ai:apitype', + '$.meta.ai:baseurl', + '$.meta.ai:apitoken', + '$.meta.ai:name', + '$.meta.ai:model', + '$.meta.ai:orgid', + '$.meta.ai:apiversion', + '$.meta.ai:maxtokens', + '$.meta.ai:timeoutms', + '$.meta.ai:fontsize', + '$.meta.ai:fixedfontsize' +) +WHERE json_extract(data, '$.meta.view') = 'waveai'; \ No newline at end of file diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index d52a11c071..3ed852a780 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -8,10 +8,10 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms, createBlock, fetchWaveFile, getApi, globalStore, useOverrideConfigAtom, WOS } from "@/store/global"; +import { atoms, createBlock, fetchWaveFile, getApi, globalStore, WOS } from "@/store/global"; import { BlockService, ObjectService } from "@/store/services"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; -import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; +import { fireAndForget, isBlank, makeIconClass, mergeMeta } from "@/util/util"; import { atom, Atom, PrimitiveAtom, useAtomValue, WritableAtom } from "jotai"; import { splitAtom } from "jotai/utils"; import type { OverlayScrollbars } from "overlayscrollbars"; @@ -67,6 +67,7 @@ export class WaveAiModel implements ViewModel { blockAtom: Atom; presetKey: Atom; presetMap: Atom<{ [k: string]: MetaType }>; + mergedPresets: Atom; aiOpts: Atom; viewIcon?: Atom; viewName?: Atom; @@ -160,22 +161,32 @@ export class WaveAiModel implements ViewModel { set(this.updateLastMessageAtom, "", false); }); - this.aiOpts = atom((get) => { + this.mergedPresets = atom((get) => { const meta = get(this.blockAtom).meta; let settings = get(atoms.settingsAtom); - settings = { - ...settings, - ...meta, - }; + let presetKey = get(this.presetKey); + let presets = get(atoms.fullConfigAtom).presets; + let selectedPresets = presets?.[presetKey] ?? {}; + + let mergedPresets: MetaType = {}; + mergedPresets = mergeMeta(settings, selectedPresets, "ai"); + mergedPresets = mergeMeta(mergedPresets, meta, "ai"); + + return mergedPresets; + }); + + this.aiOpts = atom((get) => { + const mergedPresets = get(this.mergedPresets); + const opts: WaveAIOptsType = { - model: settings["ai:model"] ?? null, - apitype: settings["ai:apitype"] ?? null, - orgid: settings["ai:orgid"] ?? null, - apitoken: settings["ai:apitoken"] ?? null, - apiversion: settings["ai:apiversion"] ?? null, - maxtokens: settings["ai:maxtokens"] ?? null, - timeoutms: settings["ai:timeoutms"] ?? 60000, - baseurl: settings["ai:baseurl"] ?? null, + model: mergedPresets["ai:model"] ?? null, + apitype: mergedPresets["ai:apitype"] ?? null, + orgid: mergedPresets["ai:orgid"] ?? null, + apitoken: mergedPresets["ai:apitoken"] ?? null, + apiversion: mergedPresets["ai:apiversion"] ?? null, + maxtokens: mergedPresets["ai:maxtokens"] ?? null, + timeoutms: mergedPresets["ai:timeoutms"] ?? 60000, + baseurl: mergedPresets["ai:baseurl"] ?? null, }; return opts; }); @@ -244,7 +255,6 @@ export class WaveAiModel implements ViewModel { onClick: () => fireAndForget(() => ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { - ...preset[1], "ai:preset": preset[0], }) ), @@ -437,8 +447,8 @@ export class WaveAiModel implements ViewModel { const ChatItem = ({ chatItemAtom, model }: ChatItemProps) => { const chatItem = useAtomValue(chatItemAtom); const { user, text } = chatItem; - const fontSize = useOverrideConfigAtom(model.blockId, "ai:fontsize"); - const fixedFontSize = useOverrideConfigAtom(model.blockId, "ai:fixedfontsize"); + const fontSize = useAtomValue(model.mergedPresets)?.["ai:fontsize"]; + const fixedFontSize = useAtomValue(model.mergedPresets)?.["ai:fixedfontsize"]; const renderContent = useMemo(() => { if (user == "error") { return ( diff --git a/frontend/util/util.ts b/frontend/util/util.ts index ae5df29e9c..04b581521a 100644 --- a/frontend/util/util.ts +++ b/frontend/util/util.ts @@ -329,6 +329,77 @@ function makeNativeLabel(platform: string, isDirectory: boolean, isParent: boole return `${fileAction} in ${managerName}`; } +function mergeMeta(meta: MetaType, metaUpdate: MetaType, prefix?: string): MetaType { + const rtn: MetaType = {}; + + // Helper function to check if a key matches the prefix criteria + const shouldIncludeKey = (key: string): boolean => { + if (prefix === undefined) { + return true; + } + if (prefix === "") { + return !key.includes(":"); + } + return key.startsWith(prefix + ":"); + }; + + // Copy original meta (only keys matching prefix criteria) + for (const [k, v] of Object.entries(meta)) { + if (shouldIncludeKey(k)) { + rtn[k] = v; + } + } + + // Deal with "section:*" keys (only if they match prefix criteria) + for (const k of Object.keys(metaUpdate)) { + if (!k.endsWith(":*")) { + continue; + } + + if (!metaUpdate[k]) { + continue; + } + + const sectionPrefix = k.slice(0, -2); // Remove ':*' suffix + if (sectionPrefix === "") { + continue; + } + + // Only process if this section matches our prefix criteria + if (!shouldIncludeKey(sectionPrefix)) { + continue; + } + + // Delete "[sectionPrefix]" and all keys that start with "[sectionPrefix]:" + const prefixColon = sectionPrefix + ":"; + for (const k2 of Object.keys(rtn)) { + if (k2 === sectionPrefix || k2.startsWith(prefixColon)) { + delete rtn[k2]; + } + } + } + + // Deal with regular keys (only if they match prefix criteria) + for (const [k, v] of Object.entries(metaUpdate)) { + if (!shouldIncludeKey(k)) { + continue; + } + + if (k.endsWith(":*")) { + continue; + } + + if (v === null || v === undefined) { + delete rtn[k]; + continue; + } + + rtn[k] = v; + } + + return rtn; +} + export { atomWithDebounce, atomWithThrottle, @@ -349,6 +420,7 @@ export { makeExternLink, makeIconClass, makeNativeLabel, + mergeMeta, sleep, stringToBase64, useAtomValueSafe,