@coffer-org/plugin-webchat 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChatWidget-B5heKPXv.js +29369 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/runtime/actions.d.ts +22 -0
- package/dist/runtime/actions.js +38 -0
- package/dist/runtime/chain-store.d.ts +33 -0
- package/dist/runtime/chain-store.js +61 -0
- package/dist/runtime/config.d.ts +2 -0
- package/dist/runtime/config.js +7 -0
- package/dist/runtime/connector.d.ts +16 -0
- package/dist/runtime/connector.js +43 -0
- package/dist/runtime/format.d.ts +1 -0
- package/dist/runtime/format.js +10 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.js +27 -0
- package/dist/runtime/send.d.ts +12 -0
- package/dist/runtime/send.js +57 -0
- package/dist/runtime/settings.d.ts +3 -0
- package/dist/runtime/settings.js +7 -0
- package/dist/schema.js +7108 -0
- package/dist/web.js +17 -0
- package/package.json +38 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { definePlugin } from '@coffer-org/sdk/plugin';
|
|
2
|
+
import { defineSettings } from '@coffer-org/sdk/settings';
|
|
3
|
+
import { field } from '@coffer-org/sdk/fields';
|
|
4
|
+
export default definePlugin({
|
|
5
|
+
id: 'webchat',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
dependsOn: ['orchestrator'],
|
|
8
|
+
settings: defineSettings({
|
|
9
|
+
label: 'webchat.settings.label',
|
|
10
|
+
fields: [
|
|
11
|
+
field.select({
|
|
12
|
+
key: 'reasoning_display',
|
|
13
|
+
label: 'webchat.settings.reasoning_display',
|
|
14
|
+
default: 'live',
|
|
15
|
+
options: [
|
|
16
|
+
{ value: 'live', title: 'webchat.settings.reasoning_display_live' },
|
|
17
|
+
{ value: 'off', title: 'webchat.settings.reasoning_display_off' },
|
|
18
|
+
],
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
2
|
+
export interface ThreadSummary {
|
|
3
|
+
convId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
lastTs: number;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
export interface HistoryMsg {
|
|
9
|
+
msgId: string;
|
|
10
|
+
role: 'user' | 'assistant';
|
|
11
|
+
text: string;
|
|
12
|
+
ts: number;
|
|
13
|
+
reasoning?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function threadsAction(_body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
16
|
+
threads: ThreadSummary[];
|
|
17
|
+
}>;
|
|
18
|
+
export declare function historyAction(body: Record<string, unknown>, caller: ActionCaller): Promise<{
|
|
19
|
+
convId: string;
|
|
20
|
+
messages: HistoryMsg[];
|
|
21
|
+
headMsgId: string | null;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { HttpError } from '@coffer-org/server/plugin-hooks';
|
|
2
|
+
import { chatIdFor, conversations, history } from "./chain-store.js";
|
|
3
|
+
const TITLE_MAX = 60;
|
|
4
|
+
function title(firstUserText) {
|
|
5
|
+
const t = firstUserText.trim().replace(/\s+/g, ' ');
|
|
6
|
+
return t.length <= TITLE_MAX ? t : `${t.slice(0, TITLE_MAX - 1)}…`;
|
|
7
|
+
}
|
|
8
|
+
export async function threadsAction(_body, caller) {
|
|
9
|
+
const chats = await conversations(caller.id);
|
|
10
|
+
const prefixLen = `u:${caller.id}:`.length;
|
|
11
|
+
return {
|
|
12
|
+
threads: chats.map((c) => ({
|
|
13
|
+
convId: c.chatId.slice(prefixLen),
|
|
14
|
+
title: title(c.firstUserText),
|
|
15
|
+
lastTs: c.lastTs,
|
|
16
|
+
count: c.count,
|
|
17
|
+
})),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function historyAction(body, caller) {
|
|
21
|
+
const convId = body['convId'];
|
|
22
|
+
if (typeof convId !== 'string' || !convId)
|
|
23
|
+
throw new HttpError(400, 'missing required field: convId');
|
|
24
|
+
const rows = await history(chatIdFor(caller.id, convId));
|
|
25
|
+
const reasoningByBot = new Map(rows.filter((m) => m.role === 'reasoning').map((m) => [m.msgId.slice(0, -2), m.text]));
|
|
26
|
+
const visible = rows.filter((m) => m.role !== 'reasoning');
|
|
27
|
+
return {
|
|
28
|
+
convId,
|
|
29
|
+
messages: visible.map((m) => ({
|
|
30
|
+
msgId: m.msgId,
|
|
31
|
+
role: m.role,
|
|
32
|
+
text: m.text,
|
|
33
|
+
ts: m.ts,
|
|
34
|
+
...(reasoningByBot.has(m.msgId) ? { reasoning: reasoningByBot.get(m.msgId) } : {}),
|
|
35
|
+
})),
|
|
36
|
+
headMsgId: visible.at(-1)?.msgId ?? null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ThreadChat, type StoredMsg } from '@coffer-org/server/thread-store';
|
|
2
|
+
import type { ConvMessage } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
+
export declare const CONNECTOR = "webchat";
|
|
4
|
+
export declare function chatIdFor(userId: string, convId: string): string;
|
|
5
|
+
export declare function chatPrefixFor(userId: string): string;
|
|
6
|
+
export declare function recordUser(m: {
|
|
7
|
+
chatId: string;
|
|
8
|
+
msgId: string;
|
|
9
|
+
sender?: string | null;
|
|
10
|
+
text: string;
|
|
11
|
+
ts: number;
|
|
12
|
+
replyToId: string | null;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function recordAssistant(m: {
|
|
15
|
+
chatId: string;
|
|
16
|
+
parentMsgId: string | null;
|
|
17
|
+
botMsgId: string | null;
|
|
18
|
+
text: string;
|
|
19
|
+
now: number;
|
|
20
|
+
}): Promise<boolean>;
|
|
21
|
+
export declare function recordReasoning(m: {
|
|
22
|
+
chatId: string;
|
|
23
|
+
botMsgId: string;
|
|
24
|
+
parentMsgId: string | null;
|
|
25
|
+
text: string;
|
|
26
|
+
now: number;
|
|
27
|
+
}): Promise<boolean>;
|
|
28
|
+
export declare function buildChain(headMsgId: string, opts: {
|
|
29
|
+
chatId: string;
|
|
30
|
+
maxDepth?: number;
|
|
31
|
+
}): Promise<ConvMessage[]>;
|
|
32
|
+
export declare function history(chatId: string, limit?: number): Promise<StoredMsg[]>;
|
|
33
|
+
export declare function conversations(userId: string): Promise<ThreadChat[]>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getThreadMessage, putThreadMessage, listThreadMessages, listThreadChats } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { getLogger } from '@coffer-org/sdk/logger';
|
|
3
|
+
const log = getLogger('webchat');
|
|
4
|
+
export const CONNECTOR = 'webchat';
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 40;
|
|
6
|
+
export function chatIdFor(userId, convId) {
|
|
7
|
+
return `u:${userId}:${convId}`;
|
|
8
|
+
}
|
|
9
|
+
export function chatPrefixFor(userId) {
|
|
10
|
+
return `u:${userId}:`;
|
|
11
|
+
}
|
|
12
|
+
export async function recordUser(m) {
|
|
13
|
+
await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.msgId, role: 'user', sender: m.sender ?? null, text: m.text, ts: m.ts, replyToId: m.replyToId });
|
|
14
|
+
}
|
|
15
|
+
export async function recordAssistant(m) {
|
|
16
|
+
if (m.botMsgId == null || !m.text.trim())
|
|
17
|
+
return false;
|
|
18
|
+
await putThreadMessage({ connector: CONNECTOR, chatId: m.chatId, msgId: m.botMsgId, role: 'assistant', text: m.text, ts: m.now, replyToId: m.parentMsgId });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
export async function recordReasoning(m) {
|
|
22
|
+
if (!m.text.trim())
|
|
23
|
+
return false;
|
|
24
|
+
await putThreadMessage({
|
|
25
|
+
connector: CONNECTOR,
|
|
26
|
+
chatId: m.chatId,
|
|
27
|
+
msgId: `${m.botMsgId}~r`,
|
|
28
|
+
role: 'reasoning',
|
|
29
|
+
text: m.text,
|
|
30
|
+
ts: m.now - 1,
|
|
31
|
+
replyToId: m.parentMsgId,
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
export async function buildChain(headMsgId, opts) {
|
|
36
|
+
const max = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
37
|
+
const acc = [];
|
|
38
|
+
let cur = headMsgId;
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
while (cur && acc.length < max && !seen.has(cur)) {
|
|
41
|
+
seen.add(cur);
|
|
42
|
+
const stored = await getThreadMessage(CONNECTOR, opts.chatId, cur);
|
|
43
|
+
if (!stored)
|
|
44
|
+
break;
|
|
45
|
+
if (stored.role !== 'reasoning') {
|
|
46
|
+
acc.push({ role: stored.role, content: stored.text, sender: stored.sender, msgId: stored.msgId, ts: stored.ts });
|
|
47
|
+
}
|
|
48
|
+
cur = stored.replyToId;
|
|
49
|
+
}
|
|
50
|
+
if (acc.length >= max)
|
|
51
|
+
log.info(`chain capped at ${max} for ${headMsgId}`);
|
|
52
|
+
while (acc.length && acc.at(-1)?.role !== 'user')
|
|
53
|
+
acc.pop();
|
|
54
|
+
return acc.reverse();
|
|
55
|
+
}
|
|
56
|
+
export function history(chatId, limit) {
|
|
57
|
+
return listThreadMessages(CONNECTOR, chatId, limit);
|
|
58
|
+
}
|
|
59
|
+
export function conversations(userId) {
|
|
60
|
+
return listThreadChats(CONNECTOR, chatPrefixFor(userId));
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Connector } from '@coffer-org/plugin-orchestrator/runtime';
|
|
2
|
+
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning } from './chain-store.ts';
|
|
3
|
+
import type { ReasoningDisplay } from './settings.ts';
|
|
4
|
+
export interface StreamingConnector {
|
|
5
|
+
connector: Connector;
|
|
6
|
+
recorded(): string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface StreamingConnectorOpts {
|
|
9
|
+
chatId: string;
|
|
10
|
+
botMsgId: string;
|
|
11
|
+
emit: (event: string, data: unknown) => void;
|
|
12
|
+
display: ReasoningDisplay;
|
|
13
|
+
record?: typeof storeAssistant;
|
|
14
|
+
recordReasoning?: typeof storeReasoning;
|
|
15
|
+
}
|
|
16
|
+
export declare function makeStreamingConnector(opts: StreamingConnectorOpts): StreamingConnector;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { makeLiveChannel, plainRender } from '@coffer-org/plugin-orchestrator/runtime';
|
|
2
|
+
import { recordAssistant as storeAssistant, recordReasoning as storeReasoning } from "./chain-store.js";
|
|
3
|
+
const WEBCHAT_THROTTLE_MS = 250;
|
|
4
|
+
const WEBCHAT_MAX = 100_000;
|
|
5
|
+
export function makeStreamingConnector(opts) {
|
|
6
|
+
const record = opts.record ?? storeAssistant;
|
|
7
|
+
const recordReasoningFn = opts.recordReasoning ?? storeReasoning;
|
|
8
|
+
let recordedId = null;
|
|
9
|
+
const connector = {
|
|
10
|
+
id: 'webchat',
|
|
11
|
+
reply(_chatId, _ctx) {
|
|
12
|
+
const ch = makeLiveChannel({
|
|
13
|
+
ops: {
|
|
14
|
+
send: async (text) => { opts.emit('message', { msgId: opts.botMsgId, text }); return opts.botMsgId; },
|
|
15
|
+
edit: async (msgId, text) => { opts.emit('delta', { msgId, text }); },
|
|
16
|
+
},
|
|
17
|
+
throttleMs: WEBCHAT_THROTTLE_MS,
|
|
18
|
+
maxLength: WEBCHAT_MAX,
|
|
19
|
+
render: plainRender(WEBCHAT_MAX),
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
...ch,
|
|
23
|
+
updateReasoning(acc) {
|
|
24
|
+
if (opts.display === 'off')
|
|
25
|
+
return;
|
|
26
|
+
opts.emit('reasoning', { msgId: opts.botMsgId, text: acc });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
async recordAssistant(m) {
|
|
31
|
+
const now = Math.floor(Date.now() / 1000);
|
|
32
|
+
const wrote = await record({ chatId: opts.chatId, parentMsgId: m.parentMsgId, botMsgId: m.botMsgId, text: m.text, now });
|
|
33
|
+
recordedId = wrote ? m.botMsgId : null;
|
|
34
|
+
if (wrote && opts.display !== 'off' && m.reasoning && m.botMsgId) {
|
|
35
|
+
await recordReasoningFn({ chatId: opts.chatId, botMsgId: m.botMsgId, parentMsgId: m.parentMsgId, text: m.reasoning, now });
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
connector,
|
|
41
|
+
recorded: () => recordedId,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WEB_FORMAT: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const WEB_FORMAT = [
|
|
2
|
+
'CRITICAL OUTPUT FORMAT — MARKDOWN ONLY. Format the reply strictly as Markdown (GFM). NEVER emit raw HTML tags — they are stripped before rendering and their content may be lost.',
|
|
3
|
+
'Inline: **bold**, *italic*, `inline code`, ~~strikethrough~~. Links: [text](https://…).',
|
|
4
|
+
'Blocks: blank-line-separated paragraphs, `#`–`###` headings, `>` blockquotes, `---` separators.',
|
|
5
|
+
'Lists: `- item` for bullets, `1. item` for numbered steps. Nest with two spaces.',
|
|
6
|
+
'Tables render as real tables — use GFM pipe syntax with a header separator row.',
|
|
7
|
+
'Code blocks: triple backticks with a language tag (```python) — highlighted on render.',
|
|
8
|
+
'The reply is read in a narrow chat panel (~360px) — keep it short and skimmable: short paragraphs, lists over prose, narrow tables.',
|
|
9
|
+
'Use Unicode symbols and emoji to structure replies: arrows (→), status marks (✅ ⚠️ ❌ ⏳), topic emoji (📅 💰 🏥 🛒 🌱 …).',
|
|
10
|
+
].join('\n');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginHooks } from '@coffer-org/server/plugin-hooks';
|
|
2
|
+
export type { ThreadSummary, HistoryMsg } from './actions.ts';
|
|
3
|
+
export { WEB_FORMAT } from './format.ts';
|
|
4
|
+
export { makeStreamingConnector } from './connector.ts';
|
|
5
|
+
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain } from './chain-store.ts';
|
|
6
|
+
export { sendAction, pageContext } from './send.ts';
|
|
7
|
+
export declare const serverHooks: PluginHooks;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { pruneThreadMessages } from '@coffer-org/server/thread-store';
|
|
2
|
+
import { threadsAction, historyAction } from "./actions.js";
|
|
3
|
+
import { sendAction } from "./send.js";
|
|
4
|
+
import { CONNECTOR } from "./chain-store.js";
|
|
5
|
+
const THREAD_TTL_MS = Number(process.env['WEBCHAT_THREAD_TTL_MS'] ?? 30 * 86_400_000);
|
|
6
|
+
export { WEB_FORMAT } from "./format.js";
|
|
7
|
+
export { makeStreamingConnector } from "./connector.js";
|
|
8
|
+
export { chatIdFor, chatPrefixFor, recordUser, recordAssistant, buildChain } from "./chain-store.js";
|
|
9
|
+
export { sendAction, pageContext } from "./send.js";
|
|
10
|
+
export const serverHooks = {
|
|
11
|
+
userActions: {
|
|
12
|
+
threads: threadsAction,
|
|
13
|
+
history: historyAction,
|
|
14
|
+
},
|
|
15
|
+
streamActions: {
|
|
16
|
+
send: (body, ctx) => sendAction(body, ctx),
|
|
17
|
+
},
|
|
18
|
+
backgroundTasks: [
|
|
19
|
+
{
|
|
20
|
+
name: 'webchat-thread-prune',
|
|
21
|
+
intervalMs: 86_400_000,
|
|
22
|
+
run: async () => {
|
|
23
|
+
await pruneThreadMessages(CONNECTOR, Math.floor((Date.now() - THREAD_TTL_MS) / 1000));
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
|
|
2
|
+
import type { ActionCaller } from '@coffer-org/server/plugin-hooks';
|
|
3
|
+
export interface SendDeps {
|
|
4
|
+
handleIncoming?: typeof handleIncoming;
|
|
5
|
+
}
|
|
6
|
+
export interface StreamCtx {
|
|
7
|
+
caller: ActionCaller;
|
|
8
|
+
emit: (event: string, data: unknown) => void;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
export declare function pageContext(ctx: unknown): string;
|
|
12
|
+
export declare function sendAction(body: Record<string, unknown>, ctx: StreamCtx, deps?: SendDeps): Promise<void>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { handleIncoming } from '@coffer-org/plugin-orchestrator/runtime';
|
|
3
|
+
import { chatIdFor, recordUser, buildChain, history } from "./chain-store.js";
|
|
4
|
+
import { policy } from "./config.js";
|
|
5
|
+
import { makeStreamingConnector } from "./connector.js";
|
|
6
|
+
import { WEB_FORMAT } from "./format.js";
|
|
7
|
+
import { loadReasoningDisplay } from "./settings.js";
|
|
8
|
+
export function pageContext(ctx) {
|
|
9
|
+
if (typeof ctx !== 'object' || ctx === null)
|
|
10
|
+
return '';
|
|
11
|
+
const c = ctx;
|
|
12
|
+
const path = typeof c['path'] === 'string' ? c['path'] : '';
|
|
13
|
+
if (!path)
|
|
14
|
+
return '';
|
|
15
|
+
const library = typeof c['library'] === 'string' ? c['library'] : '';
|
|
16
|
+
const type = typeof c['type'] === 'string' ? c['type'] : '';
|
|
17
|
+
const id = typeof c['id'] === 'string' ? c['id'] : '';
|
|
18
|
+
const where = library && type && id ? `record ${library}/${type}/${id}` : library && type ? `the ${library}/${type} list` : path;
|
|
19
|
+
return `CURRENT PAGE — the user is looking at ${where} (SPA path ${path}). When they say "this", "here", or "this record", they most likely mean it. Do not mention this note unless it is relevant.`;
|
|
20
|
+
}
|
|
21
|
+
export async function sendAction(body, ctx, deps = {}) {
|
|
22
|
+
const doHandle = deps.handleIncoming ?? handleIncoming;
|
|
23
|
+
const convId = typeof body['convId'] === 'string' ? body['convId'] : '';
|
|
24
|
+
const text = typeof body['text'] === 'string' ? body['text'] : '';
|
|
25
|
+
if (!convId) {
|
|
26
|
+
ctx.emit('error', { message: 'missing required field: convId' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!text.trim()) {
|
|
30
|
+
ctx.emit('error', { message: 'missing required field: text' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const chatId = chatIdFor(ctx.caller.id, convId);
|
|
34
|
+
const msgId = randomUUID();
|
|
35
|
+
const botMsgId = randomUUID();
|
|
36
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
37
|
+
const explicitReplyTo = typeof body['replyTo'] === 'string' && body['replyTo'] ? body['replyTo'] : null;
|
|
38
|
+
const replyTo = explicitReplyTo ?? (await history(chatId, 1)).at(-1)?.msgId ?? null;
|
|
39
|
+
await recordUser({ chatId, msgId, sender: ctx.caller.id, text, ts: nowSec, replyToId: replyTo });
|
|
40
|
+
const messages = await buildChain(msgId, { chatId });
|
|
41
|
+
const { connector, recorded } = makeStreamingConnector({
|
|
42
|
+
chatId,
|
|
43
|
+
botMsgId,
|
|
44
|
+
emit: ctx.emit,
|
|
45
|
+
display: await loadReasoningDisplay(),
|
|
46
|
+
});
|
|
47
|
+
const volatileSystem = pageContext(body['context']);
|
|
48
|
+
await doHandle(connector, {
|
|
49
|
+
connectorId: 'webchat',
|
|
50
|
+
channelSystem: WEB_FORMAT,
|
|
51
|
+
...(volatileSystem ? { volatileSystem } : {}),
|
|
52
|
+
chatId,
|
|
53
|
+
sender: { id: ctx.caller.id },
|
|
54
|
+
messages,
|
|
55
|
+
}, { policy: policy() });
|
|
56
|
+
ctx.emit('done', { msgId: recorded(), parentMsgId: msgId });
|
|
57
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getPluginSettings } from '@coffer-org/server/plugin-runtime';
|
|
2
|
+
export function toReasoningDisplay(v) {
|
|
3
|
+
return v === 'off' ? 'off' : 'live';
|
|
4
|
+
}
|
|
5
|
+
export async function loadReasoningDisplay() {
|
|
6
|
+
return toReasoningDisplay((await getPluginSettings('webchat'))['reasoning_display']);
|
|
7
|
+
}
|