diff --git a/.github/workflows/stg-deploy-stack.yml b/.github/workflows/stg-deploy-stack.yml index 779304c89..c960a4ffe 100644 --- a/.github/workflows/stg-deploy-stack.yml +++ b/.github/workflows/stg-deploy-stack.yml @@ -1,4 +1,4 @@ -name: Deploy to DEV (API Only) +name: Deploy to DEV (Web + API) on: workflow_dispatch @@ -17,6 +17,20 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Install Node and PNPM + uses: ./.github/actions/install-node-pnpm + with: + node-version: ${{ env.node-version }} + pnpm-version: ${{ env.pnpm-version }} + + - name: Get cached dependencies + # cache is automatically saved after this job completes. jobs depending on this one will get the latest cached files + id: cache-step + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: ${{ runner.os }}-modules-${{ hashFiles('**/pnpm-lock.yaml') }} + - name: Install SSH Key uses: shimataro/ssh-key-action@v2 with: @@ -27,9 +41,24 @@ jobs: run: | ssh-keyscan -H ${{ secrets.SSH_SERVER }} >> ~/.ssh/known_hosts - - name: Build + - name: Install project dependencies + if: steps.cache-step.outputs.cache-hit != 'true' + run: | + ls -la + echo $NODE_ENV + pnpm install --frozen-lockfile + + - name: Build frontend + env: + INJECT_SCRIPT: ${{ secrets.INJECT_SCRIPT }} + run: | + pnpm run build:prod + cp dist/index.html dist/original.html + node .github/inject.js + + - name: Build API run: | - docker build -f Dockerfile -t ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:latest --build-arg SHA=$GITHUB_SHA . + docker build -f Dockerfile -t ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:staging --build-arg SHA=$GITHUB_SHA . docker tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:staging ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$GITHUB_SHA - name: Log in to Container Registry @@ -37,9 +66,18 @@ jobs: - name: Publish run: | - docker push ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:latest + docker push ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:staging docker push ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$GITHUB_SHA - - name: Update Service + - name: Update Frontend + uses: shallwefootball/s3-upload-action@master + with: + aws_key_id: ${{ secrets.S3_ASSET_ACCESS_KEY }} + aws_secret_access_key: ${{ secrets.S3_ASSET_SECRET_KEY }} + aws_bucket: ${{ secrets.S3_PRD_ASSET_BUCKET }} + source_dir: 'dist' + destination_dir: '' + + - name: Update Backend run: | - sh .github/deploy.sh ${{ secrets.SSH_SERVER }} + sh .github/deploy-stg.sh ${{ secrets.SSH_SERVER }} diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 000000000..36940a07f --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,21 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "label": "Launch API (Zed)", + "adapter": "JavaScript", + "program": "${ZED_WORKTREE_ROOT}/srv/start.js", + "sourceMaps": true, + "console": "integratedTerminal", + "request": "launch", + "trace": true, + "type": "node", + "outFiles": [ + "${ZED_WORKTREE_ROOT}/srv/**/*.js", + "${ZED_WORKTREE_ROOT}/common/**/*.js", + "!**/node_modules/**" + ] + } +] diff --git a/common/chat.ts b/common/chat.ts index 97503248d..48f216d00 100644 --- a/common/chat.ts +++ b/common/chat.ts @@ -14,22 +14,31 @@ export type ChatNode = { export type ChatDepths = Record -export function runPresetParsers(parsers: PresetParser[], message: string) { +export function runPresetParsers( + parsers: PresetParser[], + message: string, + opts?: { preset?: boolean } +) { let current = message || '' for (const parser of parsers) { if (!parser.text?.trim()) continue switch (parser.type) { + case 'remove-prompt': case 'remove': { - current = current.split(parser.text.replace(/\\n/g, '\n')).join('') - break + if (parser.type === 'remove-prompt' && !opts?.preset) continue + const remove = parser.text.replace(/(? = ( @@ -323,11 +324,14 @@ export async function assemblePrompt( const post = createPostPrompt(opts) const template = getTemplate(opts) + const parsers = opts.settings?.parsers || [] + const parsedLines = opts.lines.map((line) => runPresetParsers(parsers, line, { preset: true })) + let { parsed, inserts, length, sections, linesAddedCount, history, addedLines, blocks } = await injectPlaceholders(template, { opts: { ...opts, schema: opts.jsonSchema }, parts: opts.parts, - lines: opts.lines, + lines: parsedLines, history: opts.history, characters: opts.characters, lastMessage: opts.lastMessage, diff --git a/common/requests/payloads.ts b/common/requests/payloads.ts index 7f7334256..c4f145ac8 100644 --- a/common/requests/payloads.ts +++ b/common/requests/payloads.ts @@ -378,6 +378,15 @@ function getBasePayload(opts: MinOpts, stops: string[] = []) { } if (format === 'llamacpp') { + const budget = + reasoningEffort === 'none' + ? 0 + : reasoningEffort === 'low' + ? gen.maxTokens! * 0.2 + : reasoningEffort === 'medium' + ? gen.maxTokens! * 0.5 + : gen.maxTokens! * 0.75 + const body = { prompt: messages ? undefined : prompt, messages, @@ -401,6 +410,7 @@ function getBasePayload(opts: MinOpts, stops: string[] = []) { tfs_z: gen.tailFreeSampling, json_schema, reasoning_effort: reasoningEffort, + thinking_budget_tokens: budget, chat_template_kwargs: { enable_thinking: reasoningEffort !== 'none' }, } return body @@ -648,7 +658,7 @@ export function toImageJinjaTemplate(opts: { jinja?: string; format?: ModelForma {%- if message['content'] is string %} {{- message['content'] }} {%- else %} - + {%- for block in message['content'] %} {%- if block['type'] == 'text' %} {{- block['text'] }} @@ -658,7 +668,7 @@ export function toImageJinjaTemplate(opts: { jinja?: string; format?: ModelForma {{- raise_exception('Only text and image blocks are supported in message content!') }} {%- endif %} {%- endfor %} - + {%- endif %} {%- elif message['role'] == 'system' %} {{- message['content'] }} diff --git a/common/types/image-schema.ts b/common/types/image-schema.ts index 9a71bb439..13d85e3e1 100644 --- a/common/types/image-schema.ts +++ b/common/types/image-schema.ts @@ -12,6 +12,7 @@ export type BaseImageSettings = { suffix?: string negative?: string + autofix?: boolean template?: string clipSkip?: number width: number diff --git a/common/types/presets.ts b/common/types/presets.ts index 0b6ea06e9..6bb78f320 100644 --- a/common/types/presets.ts +++ b/common/types/presets.ts @@ -103,7 +103,11 @@ export interface UserGenPreset extends GenSettings { userId: string } -export type PresetParser = { type: 'replace' | 'remove'; text: string; to?: string } +export type PresetParser = { + type: 'replace' | 'remove' | 'remove-prompt' | 'replace-prompt' + text: string + to?: string +} export type Sampler = | 'topK' diff --git a/common/types/schema.ts b/common/types/schema.ts index 4a4abbe7c..bc52e2eb3 100644 --- a/common/types/schema.ts +++ b/common/types/schema.ts @@ -362,6 +362,7 @@ export namespace AppSchema { // Emphemeral (lost on page refresh): reasoning?: string + deleted?: boolean } export type ScenarioEventType = 'world' | 'character' | 'hidden' | 'ooc' diff --git a/srv/adapter/kobold.ts b/srv/adapter/kobold.ts index d9612b254..b7a100025 100644 --- a/srv/adapter/kobold.ts +++ b/srv/adapter/kobold.ts @@ -125,15 +125,13 @@ export const handleThirdParty: ModelAdapter = async function* (opts) { } } - if (opts.gen.service === 'kobold') { - let meta: any = { - fmt: opts.gen.thirdPartyFormat, - wait, - time: round((Date.now() - start) / 1000, 2), - } - if (body.model) meta.model = body.model - yield { meta } + let meta: any = { + fmt: opts.conn.format, + wait, + time: round((Date.now() - start) / 1000, 2), } + if (body.model) meta.model = body.model + yield { meta } const parsed = sanitise(accum) const trimmed = trimResponseV2(parsed, opts.replyAs, members, opts.gen, stop_sequence) diff --git a/srv/adapter/openai.ts b/srv/adapter/openai.ts index ac9d3811f..b9033f3c3 100644 --- a/srv/adapter/openai.ts +++ b/srv/adapter/openai.ts @@ -401,7 +401,8 @@ function patchPayload(opts: AdapterProps, body: any, messages: CompletionItem { setTextStreamHeaders(res, ents, body, userMsg) + const started = Date.now() const chatStream = await createChatStream( { ...body, @@ -304,6 +305,7 @@ export const generateMessageV2 = handle(async (req, res) => { adapter = metadata.adapter meta = { + tts: 0, ctx: metadata.settings.maxContextLength, char: metadata.size, len: metadata.length, @@ -333,6 +335,7 @@ export const generateMessageV2 = handle(async (req, res) => { } if (typeof gen === 'string') { + if (meta.tts === 0) meta.tts = round((Date.now() - started) / 1000) generated = gen continue } @@ -342,11 +345,13 @@ export const generateMessageV2 = handle(async (req, res) => { } if ('tokens' in gen) { + if (meta.tts === 0) meta.tts = round((Date.now() - started) / 1000) generated = gen.tokens as string break } if ('partial' in gen) { + if (meta.tts === 0) meta.tts = round((Date.now() - started) / 1000) const prefix = body.kind === 'continue' ? `${body.continuing.msg} ` : '' if (metadata.json && schema) { jsonPartial = parsePartialJson(gen.partial, aliases) || jsonPartial diff --git a/srv/db/chats.ts b/srv/db/chats.ts index a904c0b08..5b63d6be0 100644 --- a/srv/db/chats.ts +++ b/srv/db/chats.ts @@ -6,6 +6,7 @@ import { now } from './util' import { StatusError, errors } from '../api/wrap' import { parseTemplate } from '/common/template-parser' import { config } from '../config' +import { sortAsc } from '/common/chat' export async function getChatOnly(id: string) { const chat = await db('chat').findOne({ _id: id }) @@ -19,6 +20,8 @@ export async function getChatGraph(id: string) { .project({ _id: 1, parent: 1, createdAt: 1 }) .toArray()) as Array> + messages.sort(sortAsc) + return { chat, messages } } diff --git a/web/Navigation.tsx b/web/Navigation.tsx index f39a48302..51b701ed2 100644 --- a/web/Navigation.tsx +++ b/web/Navigation.tsx @@ -184,10 +184,10 @@ const Navigation: Component = () => { aria-label="Agnaistic main page" > @@ -745,13 +745,13 @@ export const UserProfile = () => { }} > + Darkl - * > a.active { - @apply bg-[var(--hl-900)]; + @apply border-l-2 border-[var(--hl-400)] bg-[var(--hl-800)]; + padding-left: calc(0.5rem - 2px); } @keyframes hideDrawer { diff --git a/web/asset/ImageSettingsIcon.svg b/web/asset/ImageSettingsIcon.svg index 1234ce1ae..f06d66d6d 100644 --- a/web/asset/ImageSettingsIcon.svg +++ b/web/asset/ImageSettingsIcon.svg @@ -1,29 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + diff --git a/web/icons/ImageSettingsIcon.tsx b/web/icons/ImageSettingsIcon.tsx index e69de29bb..45f5e7e12 100644 --- a/web/icons/ImageSettingsIcon.tsx +++ b/web/icons/ImageSettingsIcon.tsx @@ -0,0 +1,7 @@ +import { Component } from 'solid-js' +import AppIcon, { IconProps } from './AppIcon' +import svg from 'bundle-text:../asset/ImageSettingsIcon.svg' + +const ImageSettingsIcon: Component = (props) => + +export default ImageSettingsIcon diff --git a/web/pages/Admin/SubscriptionList.tsx b/web/pages/Admin/SubscriptionList.tsx index 6f0a5181a..159381121 100644 --- a/web/pages/Admin/SubscriptionList.tsx +++ b/web/pages/Admin/SubscriptionList.tsx @@ -172,15 +172,32 @@ const SubscriptionList: Component = () => {
- [Level: {sub.subLevel}] {getServiceName(sub.service)} + + {sub.subLevel} + - {sub.name} +
+
{sub.name}
+
+ {sub.subModel} +
+
- {sub.description} + + {sub.description} + - {sub.isDefaultSub ? ' default' : ''} - {sub.subDisabled ? ' (disabled)' : ''} + + + default + + + + + disabled + + { const msgs = msgStore((s) => ({ msgs: s.msgs, textBeforeGenMore: s.textBeforeGenMore, + graph: s.graph, + cutoff: s.messageCutoffId, })) const showPane = useValidChatPane() @@ -136,7 +139,12 @@ const ChatDetail: Component = () => { const doShowHiddenEvents = showHiddenEvents() - const filtered = msgs.msgs.filter((msg) => { + const leafId = chats.chat.treeLeafId || msgs.msgs.slice(-1)[0]?._id || '' + const path = resolveChatPath(msgs.graph.tree, leafId) + + const startIndex = msgs.cutoff ? path.findIndex((msg) => msg._id === msgs.cutoff) : 0 + + const filtered = path.slice(startIndex).filter((msg) => { if (chats.opts.hideOoc && msg.ooc) return false if (msg.event === 'hidden' && !doShowHiddenEvents) return false return true @@ -579,7 +587,8 @@ const ChatDetail: Component = () => { } show={chats.opts.modal === 'restart'} confirm={() => { - msgStore.fork('root') + const first = msgs.msgs[0] + chatStore.forkChat(first?._id || '') chatStore.option({ modal: 'none' }) }} close={() => chatStore.option({ modal: 'none' })} diff --git a/web/pages/Chat/ChatFooter.tsx b/web/pages/Chat/ChatFooter.tsx index af50253bb..e45aeaf40 100644 --- a/web/pages/Chat/ChatFooter.tsx +++ b/web/pages/Chat/ChatFooter.tsx @@ -25,14 +25,10 @@ export const ChatFooter: Component<{ }> = (props) => { const user = userStore((s) => ({ profile: s.profile })) const response = responseStore((s) => ({ waiting: s.waiting })) - const msgs = msgStore((s) => ({ attachments: s.attachments })) + const msgs = msgStore((s) => ({ attachments: s.attachments, cutoff: s.messageCutoffId })) const chars = characterStore((s) => ({ botMap: s.characters.map })) const chats = chatStore((s) => ({ opts: s.opts, - char: props.ctx.active?.char, - chat: props.ctx.active?.chat, - replyAs: props.ctx.active?.replyAs, - participantIds: props.ctx.active?.participantIds, members: s.chatProfiles, })) @@ -46,22 +42,22 @@ export const ChatFooter: Component<{ }) const isGroupChat = createMemo(() => { - if (!chats.participantIds?.length) return false + if (!props.ctx.active?.participantIds?.length) return false return true }) const isSelfRemoved = createMemo(() => { if (!user.profile) return false - if (!chats.chat) return false + if (!props.ctx.active?.chat) return false const isMember = - chats.chat.userId === user.profile.userId || + props.ctx.active?.chat?.userId === user.profile.userId || chats.members.some((mem) => mem.userId === user.profile?.userId) return !isMember }) - const moreMessage = () => responseStore.continuation(chats.chat?._id!) + const moreMessage = () => responseStore.continuation(props.ctx.active?.chat?._id!) const requestRandom = () => { const index = Math.floor(Math.random() * props.pills.length) @@ -79,7 +75,7 @@ export const ChatFooter: Component<{ You have been removed from the conversation
- 1 && !!chats.chat}> + 1 && !!props.ctx.active?.chat}>
)} @@ -109,7 +105,7 @@ export const ChatFooter: Component<{
- +
@@ -130,12 +126,13 @@ export const ChatFooter: Component<{
+ { - msgStore.fork(store.clicked) + chatStore.forkChat(store.clicked) props.close() }} > @@ -216,7 +216,11 @@ export const ChatGraphModal: Component<{
- diff --git a/web/pages/Chat/components/InputBar.tsx b/web/pages/Chat/components/InputBar.tsx index a39f128d7..a2faaddb3 100644 --- a/web/pages/Chat/components/InputBar.tsx +++ b/web/pages/Chat/components/InputBar.tsx @@ -57,6 +57,7 @@ import { MsgAttachment } from '/srv/adapter/type' import { extractReasoning } from '/common/reasoning' import { usePresetContext } from '/web/store/preset-context' import { debug } from '/common/debug' +import { Pill } from '/web/shared/Card' export type SendFunc = (opts: { msg: string @@ -95,7 +96,6 @@ const InputBar: Component<{ msgs: s.msgs, canCaption: s.canImageCaption, })) - const chats = chatStore((s) => ({ replyAs: s.details[s.lastChatId]?.replyAs })) const chars = characterStore((s) => ({ impersonating: s.impersonating })) useEffect(() => { @@ -148,7 +148,7 @@ const InputBar: Component<{ const placeholder = createMemo(() => { if (props.ooc) return 'Send a message... (OOC)' - if (chats.replyAs) return `Send a message to ${ctx.allBots[chats.replyAs]?.name}...` + if (ctx.replyAs) return `Send a message to ${ctx.allBots[ctx.replyAs]?.name}...` return `Send a message...` }) @@ -236,8 +236,7 @@ const InputBar: Component<{ } const triggerEvent = () => { - const char = - chats.replyAs && chats.replyAs in props.botMap ? props.botMap[chats.replyAs] : undefined + const char = ctx.replyAs && ctx.replyAs in props.botMap ? props.botMap[ctx.replyAs] : undefined eventStore.triggerEvent(props.chat, char) setMenu(false) @@ -304,8 +303,12 @@ const InputBar: Component<{ <>
-
+
+ + +
+ + Leaf:  + {ctx.active?.chat?.treeLeafId?.slice(0, 4)} + + + + Cutoff:  + {ctx.messageCutoffId.slice(0, 4)} + +
+
@@ -384,7 +400,7 @@ const InputBar: Component<{ placeholder={placeholder()} parentClass="flex w-full" classList={{ 'blur-md': dragging() }} - class="input-bar max-h-[120px] min-h-[40px] rounded-r-none !border-0 hover:bg-[var(--bg-800)] active:bg-[var(--bg-800)]" + class="input-bar max-h-[120px] min-h-[40px] rounded-r-none !border-0 !outline-0 hover:bg-[var(--bg-800)] active:bg-[var(--bg-800)]" onKeyDown={(ev) => { if (ev.key === '@') { setComplete(true) @@ -451,7 +467,7 @@ const InputBar: Component<{ schema="secondary" size="sm" onClick={() => setAutoReplyAs('')} - disabled={!chats.replyAs} + disabled={!ctx.replyAs} > None @@ -461,7 +477,7 @@ const InputBar: Component<{ schema="secondary" size="sm" onClick={() => setAutoReplyAs(char._id)} - disabled={chats.replyAs === char._id} + disabled={ctx.replyAs === char._id} > {char.name} diff --git a/web/pages/Chat/components/ManageMemoryBooks.tsx b/web/pages/Chat/components/ManageMemoryBooks.tsx new file mode 100644 index 000000000..da338e4c1 --- /dev/null +++ b/web/pages/Chat/components/ManageMemoryBooks.tsx @@ -0,0 +1,189 @@ +import { Component, createMemo, createSignal, For } from 'solid-js' +import { memoryStore } from '/web/store/memory' +import { sortAlpha } from '/common/util' +import { AppSchema } from '/common/types' +import { createStore } from 'solid-js/store' +import { emptyBook } from '/common/memory' +import Modal from '/web/shared/Modal' +import Button from '/web/shared/Button' +import EditMemoryForm, { EntrySort } from '../../Memory/EditMemory' +import Select, { Option } from '/web/shared/Select' +import { Pencil, PlusIcon, X } from 'lucide-solid' +import { Pill } from '/web/shared/Card' + +export const ManageMemoryBooks: Component<{ + bookIds: string + updateIds: (bookIds: string) => void +}> = (props) => { + const books = memoryStore((s) => ({ + books: s.books, + items: s.books.list.map((book) => ({ label: book.name, value: book._id })), + embeds: s.embeds, + })) + + const [bookId, setBookId] = createSignal('') + const [state, setState] = createStore(emptyBook()) + const [openId, setOpenId] = createSignal() + const [entrySort, setEntrySort] = createSignal('creationDate') + + const updateEntrySort = (item: Option) => { + if (item.value === 'creationDate' || item.value === 'alpha') { + setEntrySort(item.value) + } + } + + const usedBooks = createMemo(() => { + const memoryId = props.bookIds || '' + const ids = memoryId.split(',').filter((id) => !!id.trim()) + const list = books.books.list.filter((item) => ids.includes(item._id)).sort(bookSorter) + const validIds = list.map((item) => item._id) + return { ids: validIds, list } + }) + + const availableBooks = createMemo(() => { + const used = usedBooks() + const set = new Set(used.ids) + + const available = books.books.list + .filter((item) => !set.has(item._id)) + .map((item) => ({ label: item.name, value: item._id })) + .sort(selectSorter) + + return [{ label: 'Select Book...', value: '' }].concat(available) + }) + + const removeBook = async (removeId: string) => { + const nextId = usedBooks() + .ids.filter((id) => id !== removeId) + .join(',') + + props.updateIds(nextId) + } + + const addBook = async (addId: string) => { + const nextId = usedBooks().ids.concat(addId).join(',') + + useMemoryBook(nextId) + } + + const useMemoryBook = (addBookId?: string) => { + if (!addBookId) return + + const alreadyAssigned = usedBooks().ids.includes(addBookId) + if (alreadyAssigned) return + + const nextId = usedBooks().ids.concat(addBookId).join(',') + props.updateIds(nextId) + } + + const changeBook = async (id: string) => { + const match: AppSchema.MemoryBook | undefined = + id === 'new' || id === '' + ? { + _id: '', + userId: '', + entries: [], + kind: 'memory', + name: '', + description: '', + } + : books.books.list.find((book) => book._id === id) + + if (match) setState(match) + setOpenId(id) + } + + const onSaveBook = async () => { + if (!state._id) { + memoryStore.create(state, (next) => { + setState('_id', next._id) + setOpenId('') + }) + } else { + await memoryStore.update(state._id, state) + setOpenId('') + } + } + + return ( + <> +
+
+ + - Chat Memory Books{' '} - -
- } - items={availableBooks()} - value={bookId()} - onChange={(item) => setBookId(item.value)} - /> -
- -
    - - {(book) => ( -
  • - -
    {book.name}
    - -
    -
  • - )} -
    -
+ { + if (!props.chat?._id) return + chatStore.editChat(props.chat?._id, { memoryId: next }) + }} + /> 0}> @@ -260,39 +111,9 @@ const ChatMemoryModal: Component<{ - - setOpenId('')} - title="Memory Book Editor" - maxWidth="full" - footer={ - <> - - - - } - > -
- -
-
) } export default ChatMemoryModal - -const bookSorter = sortAlpha({ prop: 'name', ignoreCase: true }) -const selectSorter = sortAlpha<{ label: string }>({ prop: 'label', ignoreCase: true }) diff --git a/web/pages/Chat/components/Message.tsx b/web/pages/Chat/components/Message.tsx index 8419d0b6f..397836e92 100644 --- a/web/pages/Chat/components/Message.tsx +++ b/web/pages/Chat/components/Message.tsx @@ -245,7 +245,7 @@ const Message: Component = (props) => { setEditSender(JSON.stringify({ characterId: message.characterId })) } if (editRef) { - editRef.innerText = message.msg + editRef.innerText = props.content } editRef?.focus() } @@ -355,7 +355,7 @@ const Message: Component = (props) => { > - {msg().parent?.slice(0, 4) || 'root'} + {msg().parent?.slice(0, 4) || 'root'} #{props.index} @@ -643,7 +643,7 @@ const Message: Component = (props) => {
{ @@ -793,7 +793,10 @@ const MessageOptions: Component<{ class: 'fork-btn', show: !props.last, outer: props.ui.msgOptsInline.fork, - onClick: () => !props.partial && msgStore.fork(props.msg._id), + onClick: () => { + if (props.partial) return + chatStore.forkChat(props.msg._id) + }, icon: Split, }, @@ -1181,7 +1184,7 @@ function wrapWithQuoteElement(str: string) { Regex magic explained: <[\s\S]*?> - skip all HTML tags eg. ```[\s\S]*?``` - skip all code blocks eg.
/``` markdown transform 
 to ```
-    ``[\s\S]*?``    - skip all inline code eg. /`` markdown transform  to `` | this is a non standard markup 
+    ``[\s\S]*?``    - skip all inline code eg. /`` markdown transform  to `` | this is a non standard markup
     `[\s\S]*?`      - skip all inline code eg. /` markdown transform  to `
 
     (\".+?\")       - capture all regular double quotes, which are not part of HTML tags or code blocks
diff --git a/web/pages/Chat/helpers.tsx b/web/pages/Chat/helpers.tsx
index 76ac88ca5..208eb624b 100644
--- a/web/pages/Chat/helpers.tsx
+++ b/web/pages/Chat/helpers.tsx
@@ -39,7 +39,8 @@ export const SwipeMessage: Component<{
 export const LoadMore: Component<{ canFetch?: boolean }> = (props) => {
   const state = msgStore((s) => ({
     msgs: s.msgs,
-    history: s.messageHistory,
+    showMore: s.msgs[0]?._id !== s.messageCutoffId,
+    cutoff: s.messageCutoffId,
   }))
   const chat = chatStore((s) => ({ loaded: s.detailLoaded }))
 
@@ -48,7 +49,7 @@ export const LoadMore: Component<{ canFetch?: boolean }> = (props) => {
       
{ msgStore.getNextMessages() }} diff --git a/web/pages/Chat/util.ts b/web/pages/Chat/util.ts index f0c185bcd..ec9190ec3 100644 --- a/web/pages/Chat/util.ts +++ b/web/pages/Chat/util.ts @@ -8,7 +8,7 @@ export type ParticipantList = ReturnType> export function useParticipantList(forChat?: boolean) { const self = getStore('user')((s) => ({ profile: s.profile })) - const msgs = getStore('messages')((s) => ({ msgs: s.msgs, messageHistory: s.messageHistory })) + const msgs = getStore('messages')((s) => ({ msgs: s.msgs })) const chars = getStore('character')((s) => ({ impersonating: s.impersonating, characters: forChat ? s.chatChars : s.characters, @@ -33,7 +33,7 @@ export function useParticipantList(forChat?: boolean) { const ids = new Set(active.map((chr) => chr._id)) - for (const msg of msgs.messageHistory.concat(msgs.msgs)) { + for (const msg of msgs.msgs) { if (!msg.characterId) continue if (state.active?.chat.tempCharacters?.[msg.characterId]) continue if (ids.has(msg.characterId)) continue diff --git a/web/pages/Image/ImageModal.tsx b/web/pages/Image/ImageModal.tsx index c9a61d3f3..b765a6bd1 100644 --- a/web/pages/Image/ImageModal.tsx +++ b/web/pages/Image/ImageModal.tsx @@ -271,6 +271,10 @@ const ImageCollectionModal: Component<{}> = (props) => { append: true, onImage: (image) => reel.reload(), onPrompt: (prompt) => update('prompt', prompt), + onTick: (text, state) => { + if (state !== 'partial' && state !== 'done') return + update('prompt', text) + }, }) } else { const result = await imageApi.generateImageAsync(imagePrompt, { diff --git a/web/pages/Profile/SubscriptionPage.tsx b/web/pages/Profile/SubscriptionPage.tsx index ce94061aa..bc764e939 100644 --- a/web/pages/Profile/SubscriptionPage.tsx +++ b/web/pages/Profile/SubscriptionPage.tsx @@ -213,7 +213,7 @@ export const SubscriptionPage: Component<{}> = (props) => { {(each) => ( <> - + This tier is currently gifted to you
diff --git a/web/pages/Settings/Image/ImageSettings.tsx b/web/pages/Settings/Image/ImageSettings.tsx index caeb0a8e3..f62bfcf62 100644 --- a/web/pages/Settings/Image/ImageSettings.tsx +++ b/web/pages/Settings/Image/ImageSettings.tsx @@ -287,6 +287,13 @@ export const ImageSettingsModal = () => { onChange={(ev) => ctx.update('cfg', ev)} /> + ctx.update('autofix', ev)} + /> + setClicked(false), 1000) diff --git a/web/shared/CustomSelect.tsx b/web/shared/CustomSelect.tsx index 79bf56c6a..f37f9b931 100644 --- a/web/shared/CustomSelect.tsx +++ b/web/shared/CustomSelect.tsx @@ -60,6 +60,7 @@ export const CustomSelect: Component<{ closeSub?: ComponentSubscriber<'close'> openSub?: ComponentSubscriber<'open'> actions?: OptionAction[] + searchText?: (input: string) => void }> = (props) => { const [open, setOpen] = createSignal(false) const [filter, setFilter] = createSignal('') @@ -187,7 +188,10 @@ export const CustomSelect: Component<{ parentClass="text-sm" fieldName="options-filter" placeholder="Filter..." - onChange={(ev) => setFilter(ev.currentTarget.value)} + onChange={(ev) => { + setFilter(ev.currentTarget.value) + props.searchText?.(ev.currentTarget.value) + }} value={filter()} /> diff --git a/web/shared/PhraseBias.tsx b/web/shared/PhraseBias.tsx index 091cca04b..f8d0b1f4f 100644 --- a/web/shared/PhraseBias.tsx +++ b/web/shared/PhraseBias.tsx @@ -178,7 +178,9 @@ export const MessageParsers: Field = (props) => {